Programming lesson
FUSE-Dateisysteme und WAD-Dateiformat: Eine praktische Einführung mit DOOM-Mods
Lerne, wie du mit FUSE ein benutzerspace Dateisystem erstellst, das WAD-Dateien aus DOOM als Verzeichnisstruktur bereitstellt – ideal für Geheimdatenübertragung oder Retro-Gaming-Projekte.
Einleitung: Von DOOM-Mods zu versteckten Dateisystemen
Stell dir vor, du bist ein Doppelagent, der in einer feindlichen Organisation untergetaucht ist – und deine einzige Möglichkeit, Nachrichten nach draußen zu schmuggeln, sind die WAD-Dateien des Kultspiels DOOM. Genau das Szenario der aktuellen Aufgabe in COP4600: File Systems. Aber keine Sorge, du musst keine echten Geheimdienste unterstützen – stattdessen tauchst du in die Welt der FUSE (Filesystem in UserSpacE) ein und lernst, wie man ein Dateisystem im Benutzermodus bereitstellt, das auf dem WAD-Format basiert.
In diesem Tutorial zeige ich dir die Grundlagen des WAD-Formats, wie du es mit einer C++-Bibliothek ausliest und wie die FUSE-Schnittstelle funktioniert. Am Ende wirst du verstehen, wie du DOOM-Moddaten als normale Ordner und Dateien im Dateimanager anzeigen lassen kannst – und das alles, ohne den Kernel zu modifizieren. Das ist besonders nützlich, wenn du mit Retro-Gaming-Dateien arbeitest oder eigene Projekte mit benutzerdefinierten Dateisystemen umsetzen möchtest.
Das WAD-Format: Datenstruktur von DOOM
Das WAD (Where's All the Data) Format ist ein Container für Spielressourcen, das in DOOM und anderen klassischen Spielen verwendet wird. Es besteht aus drei Teilen: Header, Descriptors und Lumps. Lerne diese Struktur kennen, denn sie ist der Schlüssel zu deinem Dateisystem.
Header
Der Header enthält einen Magic-String (z.B. IWAD oder PWAD), die Anzahl der Descriptors und den Offset, an dem die Descriptor-Liste beginnt. Alle Zahlen sind im Little-Endian-Format gespeichert, was auf den meisten modernen Systemen bereits der Fall ist – du musst also keine Byte-Reihenfolge umkehren.
struct WadHeader {
char magic[4]; // z.B. "IWAD"
uint32_t numDescriptors;
uint32_t descriptorOffset;
};Descriptors
Jeder Descriptor beschreibt einen Lump (Datenblock) mit Offset, Länge und Namen. Manche Descriptoren haben spezielle Namenskonventionen, die sie als Marker für Verzeichnisse kennzeichnen.
struct WadDescriptor {
uint32_t lumpOffset;
uint32_t lumpSize;
char name[8]; // Null-terminiert, aufgefüllt mit \0
};Marker: Verzeichnisse im WAD
Es gibt zwei Arten von Markern: Map-Marker (z.B. E1M1) und Namespace-Marker (z.B. F1_START und F1_END). Map-Marker werden gefolgt von genau 10 Descriptoren, die zur Map gehören. Namespace-Marker umschließen einen Bereich von Descriptoren, die in einem Ordner mit dem Namen des Namespace (ohne Suffix) gruppiert werden.
Beispiel: Die Descriptoren F1_START, LUMP1, LUMP2, F1_END erzeugen einen Ordner F1 mit den Dateien LUMP1 und LUMP2.
Implementierung einer WAD-Bibliothek in C++
Bevor wir FUSE nutzen, brauchen wir eine Klasse, die WAD-Daten ausliest. Die Aufgabenstellung definiert eine Wad-Klasse mit folgenden Methoden:
static Wad* loadWad(const string &path): Lädt eine WAD-Datei und gibt einen Zeiger auf ein neues Wad-Objekt zurück.string getMagic(): Gibt den Magic-String zurück.bool isContent(const string &path): Prüft, ob der Pfad eine Datei (Lump) oder ein Verzeichnis ist.bool isDirectory(const string &path): Prüft, ob der Pfad ein Verzeichnis ist.int getSize(const string &path): Gibt die Größe eines Lumps zurück oder -1 bei Verzeichnissen.int getContents(const string &path, char *buffer, int length, int offset): Kopiert Daten aus einem Lump in einen Buffer.
Die Bibliothek muss die Descriptor-Liste parsen und eine interne Verzeichnisstruktur aufbauen. Du kannst dazu eine Map von Pfaden zu Descriptor-Indizes verwenden. Achte darauf, dass Wurzelverzeichnis "/" ist und Pfade mit '/' getrennt werden.
FUSE: Ein Dateisystem im Benutzermodus
FUSE erlaubt es dir, ein Dateisystem zu implementieren, ohne Kernel-Module zu schreiben. Dein Programm wird ein Daemon, der sich bei FUSE registriert und Callback-Funktionen für Dateioperationen bereitstellt. Für die Aufgabe musst du mindestens getattr, readdir, open und read implementieren. Optional auch mkdir, mknod und write.
Ein typischer FUSE-Callback sieht so aus:
static int wad_getattr(const char *path, struct stat *stbuf) {
memset(stbuf, 0, sizeof(struct stat));
if (wad->isDirectory(path)) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (wad->isContent(path)) {
stbuf->st_mode = S_IFREG | 0444;
stbuf->st_nlink = 1;
stbuf->st_size = wad->getSize(path);
} else {
return -ENOENT;
}
return 0;
}Mit FUSE kannst du dann das WAD-Dateisystem an einem Mountpunkt bereitstellen. Zum Beispiel: ./wad_fuse /mnt/wad. Anschließend siehst du im Dateimanager die Ordnerstruktur der WAD-Datei.
Praktische Anwendung: Geheimdaten in DOOM-Mods
Warum ist das gerade jetzt relevant? Im Jahr 2026 erleben Retro-Games eine Renaissance, und DOOM-Mods sind beliebter denn je. Viele Entwickler nutzen WAD-Dateien, um benutzerdefinierte Level und Texturen zu erstellen. Mit deinem FUSE-Dateisystem könntest du zum Beispiel versteckte Botschaften in Texturen einbetten – genau wie in der Aufgabenstellung. Aber auch für legitime Zwecke ist es nützlich: Du kannst WAD-Inhalte durchsuchen, extrahieren oder sogar neue Dateien hinzufügen, ohne spezielle Tools zu verwenden.
Ein Beispiel aus der Praxis: Du möchtest alle Texturen eines WAD-Archivs in einen Ordner kopieren. Mit dem gemounteten Dateisystem kannst du einfach cp /mnt/wad/F1/*.png ./textures/ ausführen – vorausgesetzt, die Lumps sind als PNG erkennbar. (In echtem DOOM sind sie meist in einem eigenen Format, aber das Prinzip bleibt.)
Herausforderungen und Tipps
- Marker richtig behandeln: Achte darauf, dass Map-Marker genau 10 Descriptoren folgen und dass Namespace-Marker paarweise auftreten. Deine Bibliothek muss diese Struktur erkennen und die entsprechenden Verzeichnisse anlegen.
- Schreibzugriff: In der Aufgabenstellung darfst du nicht in bestehende Lumps schreiben, aber du kannst neue Dateien (Lumps) anlegen. Das erfordert, dass du die WAD-Datei modifizierst – ein spannender Aspekt, der über reines Lesen hinausgeht.
- Fehlerbehandlung: FUSE-Operationen müssen korrekte Fehlercodes zurückgeben (z.B.
-ENOENTfür nicht existierende Pfade). Teste dein Dateisystem mitls -la,catundtouchim gemounteten Verzeichnis.
Fazit
Mit FUSE und einer guten WAD-Bibliothek hast du ein mächtiges Werkzeug, um alte Spieldateien als vollwertiges Dateisystem bereitzustellen. Ob für Geheimdatenübertragung (wie im Lizard-Legion-Szenario) oder für Retro-Gaming-Projekte – die Technik ist vielseitig einsetzbar. Probiere es selbst aus: Lade eine DOOM-WAD-Datei herunter (z.B. die Shareware-Version von DOOM) und implementiere die grundlegenden FUSE-Callbacks. Du wirst sehen, wie einfach es ist, ein eigenes Dateisystem zu bauen.
Und denk dran: Die Widerstandskämpfer zählen auf dich! Viel Erfolg bei deinem Projekt.