Alle Visuals
FreeXP · CrossPoint · .DB1 / .IX1 · B-Tree-Index

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.

📐 .DB1-Layout🔬 Byte-Ansicht💾 C-Strukturen🌳 B-Tree▶ Animation⚖️ dBase-Vergleich
01 · Speicherformat

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

Header · 64 BKennung „DB1", Satzanzahl, felder, recsize, hdsize
7× Feld · 32 BEin 32-Byte-Deskriptor (dbfeld) pro Feld
N× Satz · 135 BDatensätze fester Breite, ab Offset 0x120

Feld-Deskriptoren (MSGS)

FeldTyp (feldtyp)Größe
INT_NR2 (Integer)4 B
BRETT1 (String)41 B
ABSENDER1 (String)31 B
BETREFF1 (String)49 B
EMPFDATUM4 (Datum)4 B
GROESSE2 (Integer)4 B
GELESEN5 (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).

0
01
00
00
00
22
/
Z
-
N
E
T
Z
/
C
O
M
P
U
T
E
R
/
A
M
I
G
A
18
A
n
d
r
e
a
s
K
o
c
h
@
G
S
2
33
W
e
l
c
h
e
S
o
u
n
d
k
a
r
t
e
f
u
e
r
d
e
n
A
1
2
0
0
?
6C
96
0F
00
33
07
00
00
1
rflagintnrbrettabsenderbetreffempfdatumgroessegelesen

→ Wegen der festen Breite springt FreeXP jeden Satz direkt an: offset = hdsize + (INT_NR − 1) × recsize

02 · C-Strukturen

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_header.h
/* .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 */
};
db1_field.h
/* 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 */
};
msg_record.h
/* 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 */
};
index_node.h
/* 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) */
};
inode_key.h
/* 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]) */
};
03 · Header & C-Reader

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).

4
4
4
4
16
2
2
2
4
22
OffsetGrößeFeldBedeutungBeispiel (MSGS.DB1)
04 BmagicDatei-Kennung "DB1" + 0x0044 42 31 00
44 BrecsAnzahl physischer Datensätze14
84 Bnextinrnächste INT_NR15
124 Bfirstfreeerster freier Satz (0 = keiner)0
1616 Buserflags[8]frei verwendbare Flags0x00 …
322 BfelderAnzahl Datenfelder7
342 Brecsizephysische Satzgröße135
362 BhdsizeBytes bis zum 1. Datensatz288
384 BreccountAnzahl gültiger Datensätze14
4222 BfillReserve0x00 …

📐 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.

FeldTypGrößeOffsetByte-Bereich
rflag10[0]
INT_NRInteger41[14]
BRETTString415[545]
ABSENDERString3146[4676]
BETREFFString4977[77125]
EMPFDATUMDatum4126[126129]
GROESSEInteger4130[130133]
GELESENUntyped1134[134134]

Satzbreite gesamt: 135 Bytes · Offset-Formel: offset[i] = offset[i−1] + feldsize[i−1]

🖥️ Beispielausgabe

$ ./db1read MSGS.DB1 1
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
db1read.c – generisches Einlesen
#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;
}
04 · Der Index

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.

Wähle einen Absender und starte die Suche. Der Index nutzt „ABSENDER" als Schlüssel. Mit 👣 Schritt bzw. den Tasten ⏭/⏮ gehst du Schritt für Schritt durch den Baum.

🌳 MSGS.IX1 B-Tree-Index (Schlüssel: ABSENDER)

aktiver KnotenVergleichTreffer
#1
Dirk Wagner @ SOL
Holger Bauer @ HH
#2
Bernd Schulze @ BIONIC
#5
Andreas Koch @ GS2
#6
Claudia Maier @ MUC
#3
Frank Becker @ ZERBERUS
#7
Eva Hoffmann @ FRACTAL
#8
Gabi Richter @ XANADU
#4
Jens Weber @ NIGHTFALL
Lars Neumann @ KA
#9
Ingrid Fischer @ CMS
#10
Karin Lang @ INDIVIDUAL
#11
Monika Klein @ GS2
Norbert Schmidt @ FRACTAL
Gelesene Indexknoten
0
= Plattenzugriffe im Index
Schlüsselvergleiche
0
statt bis zu 14 beim Full-Scan
Ergebnis
Baumhöhe 3 · 14 Datensätze

🗂️ 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_NROffsetABSENDERBRETTBETREFFDATUM
140x7FBNorbert Schmidt @ FRACTAL/FIDO/GER/CHATRe: Treffen in Hannover?950321
130x774Monika Klein @ GS2/Z-NETZ/FORUM/SYSOPNeue Gebuehren ab April950320
120x6EDLars Neumann @ KA/MAUS/COMPUTER/DOSRe: Himem.sys und EMM386 Problem950319
110x666Karin Lang @ INDIVIDUAL/CL/POLITIK/DATENSCHUTZPGP 2.6.2i und das Krypto-Verbot950319
100x5DFJens Weber @ NIGHTFALL/FIDO/GER/MODEMInit-String fuer USR Courier950318
90x558Ingrid Fischer @ CMS/Z-NETZ/COMPUTER/AMIGAAmigaOS 3.1 ROM tauschen950318
80x4D1Holger Bauer @ HH/MAUS/UMWELTRecyclingpapier-Sammelbestellung950317
70x44AGabi Richter @ XANADU/CL/COMPUTER/LINUXRe: Kernel 1.2.0 ist da!950316
60x3C3Frank Becker @ ZERBERUS/Z-NETZ/TELECOM/MODEMZyXEL U-1496 Erfahrungen950316
50x33CEva Hoffmann @ FRACTAL/FIDO/GER/CHATTreffen in Hannover?950315
40x2B5Dirk Wagner @ SOL/CL/COMPUTER/LINUXKernel 1.2.0 ist da!950315
30x22EClaudia Maier @ MUC/MAUS/COMPUTER/DOSHimem.sys und EMM386 Problem950314
20x1A7Bernd Schulze @ BIONIC/Z-NETZ/COMPUTER/AMIGARe: Welche Soundkarte fuer den A1200?950313
10x120Andreas Koch @ GS2/Z-NETZ/COMPUTER/AMIGAWelche Soundkarte fuer den A1200?950312