VisualCrossPoint
Wie funktioniert die Datenbank des legendären DOS-Offline-Readers CrossPoint / FreeXP im Inneren? Diese App zeigt die physischen .DB1-Strukturen, den passenden C-Code und – als Herzstück – den .IX1-B-Tree-Index mit animiertem, schrittweisem Zugriff. Als Beispiel dient eine typische 90er-Jahre-Mailbox.
Aufbau eines .DB1-Datensatzes
Eine FreeXP-Tabelle speichert Datensätze mit fester Breite. Aus dem Kopf der Datei und den Feld-Deskriptoren ergibt sich, wo jeder Satz und jedes Feld liegt. Strings sind Pascal-ShortStrings mit Längenbyte.
📐 Aufbau der MSGS.DB1-Datei
Feld-Deskriptoren (MSGS)
| Feld | Typ (feldtyp) | Größe |
|---|---|---|
| INT_NR | 2 (Integer) | 4 B |
| BRETT | 1 (String) | 41 B |
| ABSENDER | 1 (String) | 31 B |
| BETREFF | 1 (String) | 49 B |
| EMPFDATUM | 4 (Datum) | 4 B |
| GROESSE | 2 (Integer) | 4 B |
| GELESEN | 5 (Untyped) | 1 B |
Feld 0 ist INT_NR – die interne Satznummer, die der Index als Zeiger nutzt.
🔬 Datensatz als Bytes
Datensatz #1 liegt bei Offset 0x120. Stringfelder beginnen mit einem Längenbyte, gefolgt von den Zeichen und ␀-Füllung. Zahlen/Datum stehen binär (hex).
→ Wegen der festen Breite springt FreeXP jeden Satz direkt an: offset = hdsize + (INT_NR − 1) × recsize
Die FreeXP-Strukturen in C
So sehen die Strukturen aus (1:1-Übersetzung der Pascal-Records aus DATADEF1.PAS), mit denen ein Treiber Dateikopf, Feld-Deskriptoren, einen Nachrichten-Datensatz und die Index-Knoten im Speicher abbildet.
/* .DB1-Dateikopf - exakt 64 Bytes, am Dateianfang (Pascal: dbheader) */struct DB1_Header { char magic[4]; /* "DB1" + 0x00 (Datei-Kennung) */ int32_t recs; /* Anzahl physischer Datensaetze */ int32_t nextinr; /* naechste zu vergebende INT_NR */ int32_t firstfree; /* Nr. des ersten freien Satzes */ uint16_t userflags[8]; /* frei verwendbare Flags */ uint16_t felder; /* Anzahl Datenfelder */ uint16_t recsize; /* physische Satzgroesse in Bytes */ uint16_t hdsize; /* Bytes bis zum 1. Datensatz */ int32_t reccount; /* Anzahl gueltiger Datensaetze */ uint8_t fill[22]; /* Reserve */};/* Feld-Deskriptor - 32 Bytes je Feld, direkt nach dem Header (Pascal: dbfeld) */struct DB1_Field { char name[11]; /* Feldname (ShortString[10]) */ uint8_t fill1[5]; /* Reserve */ uint16_t feldsize; /* physische Feldgroesse in Bytes */ uint8_t feldtyp; /* 1=String 2=Int 3=Real 4=Datum 5=Untyp */ uint8_t nlen, nk; /* Stellen / Nachkommastellen (Zahlen) */ uint8_t fill2[11]; /* Reserve */};/* Ein MSGS-Datensatz - feste Breite 135 Bytes. Strings sind ShortStrings: [Laengenbyte][Zeichen...][0x00-Fuellung]. */struct MsgRecord { uint8_t rflag; /* Satz-Flag (Bit 0 = geloescht) */ int32_t int_nr; /* INT_NR - interne Satznummer */ char brett[41]; /* ShortString[40] - Brett/Newsgroup */ char absender[31]; /* ShortString[30] - Absender (Index) */ char betreff[49]; /* ShortString[48] - Betreff */ int32_t empfdatum; /* Datum (t/m/jj als LongInt) */ int32_t groesse; /* Nachrichtengroesse in Bytes */ uint8_t gelesen; /* 1 = gelesen, 0 = ungelesen */};/* Ein Knoten der .IX1-Datei (Pascal: indexnode) */struct IndexNode { uint16_t memsize; /* Groesse im Speicher (fuer FreeMem) */ uint8_t ksize, nk; /* Schlusselgroesse / max. Schluessel */ uint16_t irsize; /* Index-Recordgroesse (Knotengroesse) */ void *db_p; /* Zeiger auf die zugehoerige DB */ int32_t filepos; /* Position des Knotens in der .IX1 */ int16_t anzahl; /* Anzahl belegter Schluessel */ struct INode_Key key[5]; /* sortierte Schluessel (key[0]=links) */};/* Ein Schluessel im B-Baum-Knoten (Pascal: inodekey) */struct INode_Key { int32_t data; /* Satznummer (INT_NR) im .DB1 */ int32_t ref; /* Zeiger auf den naechsten (Kind-)Knoten */ char keystr[128]; /* Schluesselwert (ShortString[127]) */};Dateikopf verstehen & einen Datensatz dynamisch einlesen
Der Header definiert über die Feld-Deskriptoren, welche Felder mit welcher Länge existieren. Mit vereinfachtem C liest man den Kopf, berechnet die Feld-Offsets und zerlegt jeden Datensatz generisch – ShortStrings über ihr Längenbyte.
🧱 Der Dateikopf (dbheader) – 64 Bytes
Der Header steht am Dateianfang und beschreibt die Tabelle: Kennung „DB1", Satzanzahl und – entscheidend für den Direktzugriff – felder, recsize und hdsize. Direkt danach folgt für jedes Feld ein 32-Byte-Deskriptor (dbfeld).
| Offset | Größe | Feld | Bedeutung | Beispiel (MSGS.DB1) |
|---|---|---|---|---|
| 0 | 4 B | magic | Datei-Kennung "DB1" + 0x00 | 44 42 31 00 |
| 4 | 4 B | recs | Anzahl physischer Datensätze | 14 |
| 8 | 4 B | nextinr | nächste INT_NR | 15 |
| 12 | 4 B | firstfree | erster freier Satz (0 = keiner) | 0 |
| 16 | 16 B | userflags[8] | frei verwendbare Flags | 0x00 … |
| 32 | 2 B | felder | Anzahl Datenfelder | 7 |
| 34 | 2 B | recsize | physische Satzgröße | 135 |
| 36 | 2 B | hdsize | Bytes bis zum 1. Datensatz | 288 |
| 38 | 4 B | reccount | Anzahl gültiger Datensätze | 14 |
| 42 | 22 B | fill | Reserve | 0x00 … |
📐 Kopfbereich gesamt: 64 (Header) + 7×32 (Deskriptoren) = 288 Bytes → erster Datensatz bei Offset 0x120. Anders als dBase nutzt FreeXP keinen 0x0D-Terminator, sondern den Zähler felder.
🧭 Idee: aus Deskriptoren werden Offsets
Der Reader liest erst den Header, dann alle felder Deskriptoren. Aus den Feldgrößen werden die Offsets im Satz aufaddiert – damit lässt sich jede FreeXP-Tabelle generisch zerlegen, ohne pro Tabelle ein eigenes struct.
| Feld | Typ | Größe | Offset | Byte-Bereich |
|---|---|---|---|---|
| rflag | – | 1 | 0 | [0] |
| INT_NR | Integer | 4 | 1 | [1 … 4] |
| BRETT | String | 41 | 5 | [5 … 45] |
| ABSENDER | String | 31 | 46 | [46 … 76] |
| BETREFF | String | 49 | 77 | [77 … 125] |
| EMPFDATUM | Datum | 4 | 126 | [126 … 129] |
| GROESSE | Integer | 4 | 130 | [130 … 133] |
| GELESEN | Untyped | 1 | 134 | [134 … 134] |
Satzbreite gesamt: 135 Bytes · Offset-Formel: offset[i] = offset[i−1] + feldsize[i−1]
🖥️ Beispielausgabe
INT_NR = 1 BRETT = '/Z-NETZ/COMPUTER/AMIGA' ABSENDER = 'Andreas Koch @ GS2' BETREFF = 'Welche Soundkarte fuer den A1200?' EMPFDATUM = 1021548 (=950312) GROESSE = 1843 GELESEN = 1
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <stdint.h>#pragma pack(push, 1) /* exaktes Dateilayout, kein Padding */typedef struct { /* 64-Byte-Dateikopf (Pascal dbheader) */ char magic[4]; /* "DB1" + 0x00 */ int32_t recs, nextinr, firstfree; uint16_t userflags[8]; uint16_t felder; /* Anzahl Felder */ uint16_t recsize; /* Bytes pro Datensatz */ uint16_t hdsize; /* Bytes bis zum 1. Datensatz */ int32_t reccount; uint8_t fill[22];} DB1_Header;typedef struct { /* 32-Byte-Feld-Deskriptor (dbfeld) */ char name[11]; /* ShortString[10] */ uint8_t fill1[5]; uint16_t feldsize; /* physische Feldgroesse */ uint8_t feldtyp; /* 1=String 2=Int 4=Datum 5=Untyped */ uint8_t nlen, nk; uint8_t fill2[11];} DB1_Field;#pragma pack(pop)int main(int argc, char **argv) { FILE *fp = fopen("MSGS.DB1", "rb"); if (!fp) { perror("fopen"); return 1; } /* 1) Dateikopf + Kennung pruefen */ DB1_Header hdr; fread(&hdr, sizeof(hdr), 1, fp); if (memcmp(hdr.magic, "DB1", 3) != 0) { printf("Keine FreeXP-.DB1-Datei!\n"); return 1; } /* 2) Feld-Deskriptoren lesen; Offsets aus den Feldgroessen aufaddieren */ DB1_Field fields[64]; int field_offset[64]; int offset = 1; /* Byte 0 = rflag */ for (int i = 0; i < hdr.felder; i++) { fread(&fields[i], sizeof(DB1_Field), 1, fp); field_offset[i] = offset; offset += fields[i].feldsize; } /* 3) Direktzugriff auf einen Satz per Offset-Rechnung */ long intnr = (argc > 1) ? atol(argv[1]) : 1; long pos = hdr.hdsize + (intnr - 1) * hdr.recsize; fseek(fp, pos, SEEK_SET); uint8_t *rec = malloc(hdr.recsize); fread(rec, hdr.recsize, 1, fp); if (rec[0] & 1) { printf("Satz %ld ist geloescht\n", intnr); } /* 4) Felder generisch zerlegen - ShortString ueber das Laengenbyte */ for (int i = 0; i < hdr.felder; i++) { uint8_t *p = rec + field_offset[i]; if (fields[i].feldtyp == 1) { /* String */ int len = p[0]; printf("%-11s = '%.*s'\n", fields[i].name, len, p + 1); } else { /* Int / Datum / Untyped */ long v = 0; memcpy(&v, p, fields[i].feldsize); printf("%-11s = %ld\n", fields[i].name, v); } } free(rec); fclose(fp); return 0;}Index in Aktion – Select, Insert, Aufbau & mehr
Der .IX1-Index ist ein B-Baum. Select zeigt die Suche von der Wurzel zum Datensatz – in verschiedenen Geschwindigkeiten oder Schritt für Schritt. Insert demonstriert das Anhängen inkl. Knoten-Split, Index den Aufbau der .IX1, Schlüssel das Parsen eines Index-Ausdrucks, Datenbanken alle FreeXP-DBs und dBase? die Kompatibilitätsfrage.
🌳 MSGS.IX1 B-Tree-Index (Schlüssel: ABSENDER)
🗂️ MSGS.DB1 – Nachrichten
So zeigt ein Reader die Nachrichten: nach Empfangsdatum sortiert, die neuesten oben – genau das leistet der reale MSGS-Index miBrett = BRETT/EMPFDATUM/INT_NR. Die Sortierung steckt im Index, die DB1 selbst bleibt physisch unsortiert.
| INT_NR | Offset | ABSENDER | BRETT | BETREFF | DATUM ▼ |
|---|---|---|---|---|---|
| 14 | 0x7FB | Norbert Schmidt @ FRACTAL | /FIDO/GER/CHAT | Re: Treffen in Hannover? | 950321 |
| 13 | 0x774 | Monika Klein @ GS2 | /Z-NETZ/FORUM/SYSOP | Neue Gebuehren ab April | 950320 |
| 12 | 0x6ED | Lars Neumann @ KA | /MAUS/COMPUTER/DOS | Re: Himem.sys und EMM386 Problem | 950319 |
| 11 | 0x666 | Karin Lang @ INDIVIDUAL | /CL/POLITIK/DATENSCHUTZ | PGP 2.6.2i und das Krypto-Verbot | 950319 |
| 10 | 0x5DF | Jens Weber @ NIGHTFALL | /FIDO/GER/MODEM | Init-String fuer USR Courier | 950318 |
| 9 | 0x558 | Ingrid Fischer @ CMS | /Z-NETZ/COMPUTER/AMIGA | AmigaOS 3.1 ROM tauschen | 950318 |
| 8 | 0x4D1 | Holger Bauer @ HH | /MAUS/UMWELT | Recyclingpapier-Sammelbestellung | 950317 |
| 7 | 0x44A | Gabi Richter @ XANADU | /CL/COMPUTER/LINUX | Re: Kernel 1.2.0 ist da! | 950316 |
| 6 | 0x3C3 | Frank Becker @ ZERBERUS | /Z-NETZ/TELECOM/MODEM | ZyXEL U-1496 Erfahrungen | 950316 |
| 5 | 0x33C | Eva Hoffmann @ FRACTAL | /FIDO/GER/CHAT | Treffen in Hannover? | 950315 |
| 4 | 0x2B5 | Dirk Wagner @ SOL | /CL/COMPUTER/LINUX | Kernel 1.2.0 ist da! | 950315 |
| 3 | 0x22E | Claudia Maier @ MUC | /MAUS/COMPUTER/DOS | Himem.sys und EMM386 Problem | 950314 |
| 2 | 0x1A7 | Bernd Schulze @ BIONIC | /Z-NETZ/COMPUTER/AMIGA | Re: Welche Soundkarte fuer den A1200? | 950313 |
| 1 | 0x120 | Andreas Koch @ GS2 | /Z-NETZ/COMPUTER/AMIGA | Welche Soundkarte fuer den A1200? | 950312 |