====== Fremde Dateisysteme lesen ======
Lyx kann sechs Dateisysteme lesen, ohne den Kernel zu fragen: FAT, exFAT, ext2/3/4, NTFS und SquashFS — dazu die Partitionstabelle davor. Dieser Guide zeigt, wie die Units zusammenspielen, und erklärt, **warum es sechs sind und nicht eine**.
→ [[lyx_-_programmiersprache:guides|Guides]] · [[lyx_-_programmiersprache:units:fs:dateisysteme|Die Dateisystem-Sammlung]] · [[lyx_-_programmiersprache:units:fs|std.fs]] · [[lyxos:iofs|Lyx OS: IOFS]]
import std.fs.part; // MBR und GPT — wo faengt was an?
import std.fs.fat; // FAT12/16/32
import std.fs.exfat; // exFAT
import std.fs.ext4; // ext2, ext3, ext4
import std.fs.ntfs; // NTFS
import std.fs.squashfs; // SquashFS 4.0
----
===== 1. Wozu das gut ist =====
Ein Programm, das ein Dateisystem selbst liest, braucht **kein Betriebssystem darunter**, das es schon eingehängt hat. Das ist der Unterschied zwischen „ich lese eine Datei" und „ich lese eine Datei **aus einem Abbild**":
* **Forensik und Wiederherstellung** — ein Abbild untersuchen, ohne es einzuhängen und dabei zu verändern.
* **Bare-Metal und Lyx OS** — dort gibt es keinen Linux-Kernel, der die Arbeit abnimmt.
* **Prüfwerkzeuge** — ein erzeugtes Abbild gegen die Erwartung halten, ohne Root-Rechte.
* **Installer und Startmedien** — auf einem fremden Datenträger nachsehen, was darauf liegt.
----
===== 2. Der immer gleiche Ablauf =====
Alle Units arbeiten auf einem **offenen Deskriptor plus Byte-Versatz**. Ob dahinter eine Abbilddatei, eine Partition oder ein ganzes Gerät liegt, ist ihnen gleich.
unit main;
import std.fs.part;
import std.io;
import std.string;
fn main(): int64 {
var fd: int64 := open("/tmp/platte.img"c, 0, 0);
if (fd < 0) { PrintLn("Abbild fehlt"); return 1; }
var t: PartTable := PartScan(fd, 512);
if (!PartValid(t)) { PrintLn("keine Partitionstabelle"); close(fd); return 1; }
PrintLn(StrConcat("Schema: ", IntToStr(PartSchema(t))));
var i: int64 := 0;
while (i < PartCount(t)) {
var e: PartEntry;
PartGet(t, i, e);
PrintLn(StrConcat(StrConcat(" #", IntToStr(i)),
StrConcat(" Versatz ", IntToStr(PartByteOffset(t, i)))));
i := i + 1;
}
PartFree(t);
close(fd);
return 0;
}
Der Rückgabewert von ''PartByteOffset'' ist **genau das zweite Argument** von ''FatMount'', ''ExfatMount'', ''Ext4Mount'', ''NtfsMount'' und ''SquashMount''. Damit hängt die Kette:
var off: int64 := PartByteOffset(t, 0);
var vol: Ext4Volume := Ext4Mount(fd, off);
**Zuerst GPT prüfen, nicht MBR.** Eine GPT-Platte trägt bei Sektor 0 einen **Schutz-MBR** mit einem einzigen Eintrag über die ganze Platte. Wer zuerst den MBR liest, hält eine Platte mit fünf Partitionen für eine einzige riesige. ''PartScan'' macht es richtig herum — wer selbst nachbaut, sollte es wissen.
----
===== 3. Warum sechs Units und nicht eine =====
Die Namen folgen demselben Schnitt (''…Mount'', ''…DirOpen'', ''…DirNext'', ''…FindPath'', ''…ReadFile''), **der Aufbau dahinter ist jedes Mal ein anderer**:
^ Format ^ Wie eine Datei zu ihren Daten kommt ^
| **FAT** | Eine Zuordnungstabelle, in der jeder Cluster auf den nächsten zeigt — eine verkettete Liste über die Platte |
| **exFAT** | Dieselbe Idee, aber zusammenhängende Dateien stehen **gar nicht** in der Tabelle (''NoFatChain'') |
| **ext4** | Eine **Inode-Tabelle**; der Verzeichniseintrag hat nur Name und Nummer, der Rest steht im Inode — als Blockliste (ext2/3) oder Extent-Baum (ext4) |
| **NTFS** | Die **Master File Table**; jede Datei ist ein Satz aus Attributen, die Daten sind eines davon — bei kleinen Dateien **im Satz selbst** |
| **SquashFS** | **Komprimierte** Metadatenblöcke; eine Inode-Referenz ist ein Paar aus Blockadresse und Versatz im entpackten Block |
Eine gemeinsame Oberschicht müsste all das unter einen Hut zwängen und verlöre dabei genau die Eigenheiten, auf die es beim Lesen ankommt. Deshalb: sechs Units, ein gemeinsames Namensmuster, keine erzwungene Abstraktion.
----
===== 4. Die Fallen, je Format eine =====
Jede davon ist **still** — das Programm läuft weiter und liefert plausible, falsche Zahlen:
^ Format ^ Falle ^
| **exFAT** | Sektor- und Clustergröße stehen als **Zweierpotenz-Exponent** da. Wer ''9'' als Sektorgröße liest statt als 512, rechnet ab da alles falsch — und die Adressen bleiben plausibel |
| **exFAT** | Das oberste Bit der Eintragsart heißt „in Gebrauch". Wer nur auf Gleichheit prüft, **liest gelöschte Einträge mit** |
| **ext4** | Die Art steht im Verzeichniseintrag **und** im Inode. Bei alten Dateisystemen ohne ''filetype'' ist die erste 0 — dann entscheidet der Inode |
| **NTFS** | **Fixup-Werte**: am Ende jedes Sektors stehen zwei Byte, die vor dem Auswerten zurückgetauscht werden müssen. Bei kleinen Sätzen fällt das nicht auf |
| **NTFS** | **Datenläufe** tragen einen **vorzeichenbehafteten** relativen Versatz — ein Lauf kann rückwärts zeigen. Ohne Vorzeichen landet man irgendwo auf der Platte |
| **NTFS** | Dateien unter etwa 700 Byte haben **keine Cluster**; ihr Inhalt liegt im MFT-Satz. Wer nur den nicht-residenten Fall kann, liest Nullen — und die sehen aus wie eine leere Datei |
| **SquashFS** | Die Blockgrößenliste nennt die Größe **auf der Platte**, nicht die entpackte |
| **MBR** | Drei Typbytes bedeuten „erweitert" (''0x05'', ''0x0F'', ''0x85''). Wer nur ''0x05'' kennt, übersieht fast alle modernen Platten |
Alle diese Fälle sind in den Units behandelt — die Tabelle steht hier, weil sie erklärt, warum der Code an manchen Stellen umständlicher aussieht, als er sein müsste.
----
===== 5. Ein Verzeichnis auflisten =====
Am Beispiel ext4; bei den anderen Formaten heißen nur die Bezeichner anders.
unit main;
import std.fs.ext4;
import std.alloc;
import std.io;
import std.string;
fn main(): int64 {
var fd: int64 := open("/tmp/ext4.img"c, 0, 0);
if (fd < 0) { return 1; }
var vol: Ext4Volume := Ext4Mount(fd, 0);
if (!Ext4Valid(vol)) { PrintLn("kein ext-Dateisystem"); close(fd); return 1; }
var wurzel: Ext4Inode;
Ext4ReadInode(vol, EXT4_ROOT_INO, wurzel);
var name: int64 := alloc(300);
var d: Ext4Dir := Ext4DirOpen(vol, wurzel);
var e: Ext4Entry;
while (Ext4DirNext(vol, d, e, name, 300) > 0) {
PrintLn(StrConcat(name as pchar, StrConcat(" Inode ", IntToStr(e.ino))));
}
Ext4DirClose(vol, d); // erst das Verzeichnis …
Ext4InodeFree(wurzel); // … dann der Inode
free(name, 300);
close(fd);
return 0;
}
**Die Reihenfolge beim Freigeben ist nicht beliebig.** Ein ''Ext4Dir'' hält den Inode als Kopie, teilt sich mit ihm aber den ''iblock''-Zeiger. Wer den Inode freigibt, während das Verzeichnis offen ist, zieht dem Durchlauf den Boden weg.
----
===== 6. Was die Units nicht tun =====
* **Sie schreiben nicht.** Bei FAT wäre die Pflege beider FAT-Kopien nötig, bei exFAT zusätzlich Belegungsbitmap und Prüfsummen, bei ext3/ext4 und NTFS die Journalführung — und ein Schreiber, der das Journal nicht führt, macht aus einem Absturz einen Datenverlust. SquashFS ist von Haus aus nur lesbar.
* **Sie hängen nichts ein.** Es entsteht kein Mountpoint und kein VFS-Eintrag; die Units lesen Strukturen, mehr nicht. Unter Lyx OS macht das Einhängen der Kernel (→ [[lyxos:syscalls|''sys_mount'', ''sys_mount_part'']]).
* **Sie raten nicht.** Was nicht umgesetzt ist — die doppelt indirekte Blockzuordnung in ext4, komprimierte Attribute in NTFS, xattr in SquashFS —, wird **gemeldet**. Still das Falsche zu liefern wäre schlimmer als die Meldung.
* **Namen werden nach ASCII gelesen**; alles darüber wird zu ''?''.
----
===== 7. Prüfstand =====
Jede Unit hat einen eigenen Testlauf gegen **echte, im Test erzeugte Abbilder** — nicht gegen Attrappen:
^ Unit ^ Test ^ Prüfungen ^
| ''std.fs.fat'' | ''tests/fat_test.sh'' | 94 |
| ''std.fs.exfat'' | ''tests/exfat_test.sh'' | 42 |
| ''std.fs.ext4'' | ''tests/ext4_test.sh'' | 53 |
| ''std.fs.ntfs'' | ''tests/ntfs_test.sh'' | 33 |
| ''std.fs.part'' | ''tests/part_test.sh'' | 47 |
| ''std.fs.squashfs'' | ''tests/squashfs_test.sh'' | 37 |
Letzte Aktualisierung: 2026-09-05 — Seite neu angelegt; erhoben aus ''std/fs/*.lyx'', Beispiele gegen lyxc 1.2.2A übersetzt.
Prüflauf 2026-09-05 mit ''doku-pruefer.py'' (Repo-Compiler **lyxc 1.2.2A**): 7 Vollprogramme der Sammlung, 7 übersetzen, 0 echte Fehler; 53 Aufrufe gegen die ''pub fn''-Signaturen gehalten, 0 Abweichungen.