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