Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3693e03494 | ||
|
|
167e4b65fc |
@@ -5,6 +5,33 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
||||||
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-09-20
|
||||||
|
|
||||||
|
### Paged Manifest für den Hidden Vault (Aufhebung der Kapazitätsgrenze)
|
||||||
|
Dieses Release führt das **Carrier-Format V2** für den Hidden Vault ein. Es hebt die bisherige Kapazitätsgrenze von ca. 7.000 Dateien vollständig auf, führt eine skalierbare Paged-Manifest-Architektur ein und optimiert Performanz sowie Resilienz signifikant.
|
||||||
|
|
||||||
|
#### Formaterweiterung Carrier V2 (D-01 bis D-05)
|
||||||
|
- **Carrier-Format V2 mit Paged Manifest**:
|
||||||
|
- Trennung von Superblock und Inode-Tabellen: Block 0 und Block 1 enthalten nur noch den leichtgewichtigen `CarrierSuperblock` (unter Erhalt der C-02 Dual-Block Redundanz).
|
||||||
|
- Inodes werden in separaten Inode-Pages (`CarrierInodePage`, Magic `SANCTPAG`, ~1 MB Payload je Block, ca. 4.500–7.000 Inodes pro Seite) aus dem normalen Blockpool verwaltet.
|
||||||
|
- Dynamische Skalierung: Die Inode-Kapazität skaliert linear mit den verfügbaren Trägerblöcken bis zu 2^32 Blöcken.
|
||||||
|
- **D-01: Paged Loading & Fail-Soft Resilienz**:
|
||||||
|
- Erkennt automatisch V1- vs. V2-Superblöcke auf Block 0 und 1.
|
||||||
|
- Resiliente Fehlerisolation: Beschädigte oder unlesbare Inode-Seiten führen nicht zum Mount-Abbruch. Fehlerhafte Seiten werden geloggt (`error!`) und übersprungen, während alle intakten Inodes gemountet werden.
|
||||||
|
- Neuer Zähler `corrupted_pages` warnt den Benutzer beim Mounten über partielle Datenbeschädigungen.
|
||||||
|
- **D-02: Sekundärindex für Kindknoten (`children_index`)**:
|
||||||
|
- In-Memory Index `children_index: HashMap<i64, Vec<i64>>` ordnet Elternknoten-IDs ihren Kindern zu.
|
||||||
|
- Beschleunigt Pfadauflösung (`resolve_path`) und Verzeichnislisting (`read_dir`) von O(N) Tabellenscans auf O(Geschwister).
|
||||||
|
- Konsistente Nachführung bei `open`, `create_dir`, `remove_file`, `remove_dir`, `rename` und `copy`.
|
||||||
|
- **D-03: Transparente V1 → V2 Migration**:
|
||||||
|
- V1-Container werden vollkommen transparent eingelesen.
|
||||||
|
- Beim ersten Speichervorgang (`save_manifest()`) wird geprüft, ob genügend freie Blöcke für die Seitenaufteilung vorhanden sind.
|
||||||
|
- Reicht der Platz nicht aus, bricht der Vorgang sauber ab, ohne den Container zu beschädigen. Reicht der Platz, wird der Container nahtlos auf Format V2 migriert.
|
||||||
|
- **D-04 & D-05: Deterministische Seitenaufteilung & Blockfreigabe**:
|
||||||
|
- Deterministische Sortierung der Inodes nach ID garantiert reproduzierbare Seitenbelegungen.
|
||||||
|
- Dynamische Blockfreigabe: Verringert sich die Inode-Anzahl (z. B. durch Löschungen), werden überzählige Seitenblöcke mittels kryptografischem Zufallsrauschen geschreddert (`shred_carrier_block`) und an den Freispeicher zurückgegeben.
|
||||||
|
- Dynamische Block-Warnung: Die statische 80%-Manifestgrenze aus C-04 wird durch eine Warnung bei absolutem Träger-Blockmangel (< 20 Blöcke oder < 5% Restkapazität) ersetzt.
|
||||||
|
|
||||||
## [0.8.1] - 2026-09-19
|
## [0.8.1] - 2026-09-19
|
||||||
|
|
||||||
### Carrier-Manifest-Resilienz & Storage-Hygiene (Dritte Audit-Runde)
|
### Carrier-Manifest-Resilienz & Storage-Hygiene (Dritte Audit-Runde)
|
||||||
|
|||||||
Generated
+1
-1
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
||||||
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ Beim Erstellen eines Tresors wählst du zwischen zwei Sicherheitsstufen:
|
|||||||
1. **Decoy-Passwort**: Öffnet den äußeren Safe (enthält eine scheinbare Backup-Datei `system_backup.dat`).
|
1. **Decoy-Passwort**: Öffnet den äußeren Safe (enthält eine scheinbare Backup-Datei `system_backup.dat`).
|
||||||
2. **Hidden-Passwort**: Öffnet den geheimen Zweittresor (Second Safe).
|
2. **Hidden-Passwort**: Öffnet den geheimen Zweittresor (Second Safe).
|
||||||
* *(Hinweis: Schützt vor Schulterblick im Alltag; keine forensische Abstreitbarkeit gegen behördliche Entropieanalyse).*
|
* *(Hinweis: Schützt vor Schulterblick im Alltag; keine forensische Abstreitbarkeit gegen behördliche Entropieanalyse).*
|
||||||
* **Kapazitätsgrenze (C-04)**: Empfohlene Obergrenze ca. 7.000 Dateien/Verzeichnisse im Hidden Vault (~1 MB Manifest-Block). Sanctum warnt beim Mounten automatisch bei über 80% Füllstand.
|
* **Kapazität & Skalierung (Format V2)**: Dank Paged-Manifest-Architektur dynamisch skalierbar (~4.500–7.000 Dateien je 1-MB-Manifestseite, keine starre 7.000er-Grenze mehr). Sanctum warnt beim Mounten automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
|
||||||
* **Wichtig**: Notiere dir die ausgegebenen **24 Wörter des Notfallschlüssels (BIP-39)** auf der untenstehenden Notfallkarte!
|
* **Wichtig**: Notiere dir die ausgegebenen **24 Wörter des Notfallschlüssels (BIP-39)** auf der untenstehenden Notfallkarte!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
|
|||||||
Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen von Chunks zwischen Dateien oder innerhalb einer Datei führt zum Authentifizierungsfehler.
|
Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen von Chunks zwischen Dateien oder innerhalb einer Datei führt zum Authentifizierungsfehler.
|
||||||
- **Dual-Vault (Multi-Slot & Carrier)**:
|
- **Dual-Vault (Multi-Slot & Carrier)**:
|
||||||
Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Second Safe (Hidden Vault) oder CSPRNG-Dummy. Dient dem Schutz vor neugierigen Blicken oder beiläufigem Zwang im Alltag. (Hinweis: Die Trägerdatei besitzt hohe Entropie und ist forensisch nachweisbar; kein Anspruch auf juristisch unnachweisbare Abstreitbarkeit gegen behördliche Beschlagnahme).
|
Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Second Safe (Hidden Vault) oder CSPRNG-Dummy. Dient dem Schutz vor neugierigen Blicken oder beiläufigem Zwang im Alltag. (Hinweis: Die Trägerdatei besitzt hohe Entropie und ist forensisch nachweisbar; kein Anspruch auf juristisch unnachweisbare Abstreitbarkeit gegen behördliche Beschlagnahme).
|
||||||
*Kapazitätsgrenze des Hidden Vault (C-04)*: Das Dateisystem des Hidden Vaults verwaltet seine Inodes in einem dual-block rollierenden Manifest (~1 MB pro Block). Daraus ergibt sich eine empfohlene Obergrenze von ca. 7.000 Dateien und Verzeichnissen im Hidden Vault. Bei Überschreiten von 80% Füllstand (~838 KB) warnt Sanctum beim Einbinden automatisch vor Erreichen der Blockgrenze.
|
*Carrier-Format V2 mit Paged Manifest*: Das steganografische Dateisystem des Hidden Vaults nutzt eine skalierbare Paged-Manifest-Architektur. Blöcke 0 und 1 speichern den redundanten Superblock (C-02), während Inodes über dedizierte Inode-Pages (~1 MB Nutzdaten je Seite, ca. 4.500–7.000 Inodes pro Seite) dynamisch aus dem Blockpool verwaltet werden. Die Kapazität ist nicht mehr auf 7.000 Dateien limitiert, sondern skaliert dynamisch mit den verfügbaren Trägerblöcken. Robuste Fail-Soft-Resilienz (D-01) isoliert Seitenbeschädigungen, ein In-Memory Sekundärindex (D-02) beschleunigt Pfadoperationen auf O(Geschwister), und Sanctum warnt beim Einbinden automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
|
||||||
- **Dateinamen-Verschlüsselung**:
|
- **Dateinamen-Verschlüsselung**:
|
||||||
Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert (Legacy-Kompatibilität über `--legacy-names`).
|
Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert (Legacy-Kompatibilität über `--legacy-names`).
|
||||||
- **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**:
|
- **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**:
|
||||||
|
|||||||
+23
-8
@@ -1,17 +1,17 @@
|
|||||||
# Sanctum Security Audit & Remediation Log (v0.8.1)
|
# Sanctum Security Audit & Remediation Log (v0.9.0)
|
||||||
|
|
||||||
Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheitsaudits zusammen, dokumentiert die angewandten Härtungsmaßnahmen, referenziert die jeweiligen Git-Commits und benennt die zugehörigen automatisierten Regressionstests.
|
Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheitsaudits zusammen, dokumentiert die angewandten Härtungsmaßnahmen, referenziert die jeweiligen Git-Commits und benennt die zugehörigen automatisierten Regressionstests. Abschnitt 5 dokumentiert die Formaterweiterung Carrier V2 (Paged Manifest) aus v0.9.0.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Audit-Zusammenfassung
|
## Audit-Zusammenfassung
|
||||||
|
|
||||||
- **Zielversion:** Sanctum v0.8.1
|
- **Zielversion:** Sanctum v0.9.0 (Basis v0.8.1 Audit-Abschluss)
|
||||||
- **Behobene Findings:** 39 / 39 (100%)
|
- **Behobene Findings:** 39 / 39 (100%)
|
||||||
- **Test-Ergebnis:** 123 / 123 Tests erfolgreich (100% Pass Rate)
|
- **Test-Ergebnis:** 130 / 130 Tests erfolgreich (100% Pass Rate)
|
||||||
- **Quality Gates:** `cargo fmt --check` (100% sauber), `cargo clippy --all-targets -- -D warnings` (0 Warnungen)
|
- **Quality Gates:** `cargo fmt --check` (100% sauber), `cargo clippy --all-targets -- -D warnings` (0 Warnungen)
|
||||||
- **Container-Format:** Upgrade auf Container-Format V3 mit kanonischer Metadaten-Authentifizierung (HMAC-SHA256) und Chunk-Replay-Schutz (Generation-gebundenes AAD).
|
- **Container-Format:** Container-Format V3 mit kanonischer Metadaten-Authentifizierung (HMAC-SHA256) und Chunk-Replay-Schutz (Generation-gebundenes AAD).
|
||||||
- **Hidden Vault & Storage-Resilienz:** Dual-Block rollierendes Manifest (C-02), Manifest-Entkopplung mit Dirty-Tracking (C-03), Kapazitäts-Warnung (C-04), Fail-Closed Node-Name-Decryption (ST-01), rekursive Fehleraggregation (ST-02) und atomare SQLite-Transaktionen für Baum-Löschungen (ST-03).
|
- **Hidden Vault & Storage-Resilienz:** Carrier-Format V2 (Paged Manifest), Dual-Block rollierender Superblock (C-02), Manifest-Entkopplung mit Dirty-Tracking (C-03), Fail-Soft Inode-Isolation (D-01), Sekundärindex (D-02), dynamische Kapazitätsskalierung und Blockwarnung, Fail-Closed Node-Name-Decryption (ST-01), rekursive Fehleraggregation (ST-02) und atomare SQLite-Transaktionen für Baum-Löschungen (ST-03).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -108,10 +108,25 @@ Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheits
|
|||||||
- Unlesbare Knoten werden mit `warn!` geloggt und in `list_children_in_vault`, `resolve_path_in_vault` und `get_node_by_id_in_vault` fail-closed übersprungen.
|
- Unlesbare Knoten werden mit `warn!` geloggt und in `list_children_in_vault`, `resolve_path_in_vault` und `get_node_by_id_in_vault` fail-closed übersprungen.
|
||||||
- **Fehleraggregation & Transaktions-Rollback bei rekursivem Löschen (ST-02 & ST-03):**
|
- **Fehleraggregation & Transaktions-Rollback bei rekursivem Löschen (ST-02 & ST-03):**
|
||||||
- Fehler beim Löschen von Kindknoten werden nicht mehr ignoriert (`let _ =` eliminiert), sondern aggregiert und protokolliert.
|
- Fehler beim Löschen von Kindknoten werden nicht mehr ignoriert (`let _ =` eliminiert), sondern aggregiert und protokolliert.
|
||||||
- Das gesamte Löschen eines Baumes samt kryptografischem Chunk-Shredding läuft in einer atomaren SQLite-Transaktion. Schlägt ein Knoten fehl, wird der gesamte Vorgang per Rollback zurückgesetzt; verwaiste Knoten (Orphans) sind ausgeschlossen.
|
### 5. Sanctum v0.9.0 — Formaterweiterung Carrier V2 (Paged Manifest)
|
||||||
|
- **Aufhebung der C-04 Manifest-Kapazitätsgrenze:**
|
||||||
|
- In v0.8.1 war das Manifest ein einzelnes JSON-Objekt auf Block 0 und Block 1 (limitiert auf ca. 7.000 Inodes).
|
||||||
|
- Mit Format V2 wird der `CarrierSuperblock` (Block 0 & 1, dual-block rollierend C-02) von den eigentlichen Inode-Daten entkoppelt.
|
||||||
|
- Inodes werden in separaten `CarrierInodePage`-Blöcken (~1 MB Payload je Seite, ca. 4.500–7.000 Inodes pro Seite) verwaltet, dynamisch allokiert aus dem Blockpool.
|
||||||
|
- **Paged Loading & Fail-Soft Resilienz (D-01):**
|
||||||
|
- Isolierte Fehlerbehandlung: Beschädigte oder unlesbare Inode-Seiten führen nicht mehr zum Gesamtabbruch des Mounts.
|
||||||
|
- Das System lädt alle lesbaren Seiten, loggt fehlerhafte Seiten als `error!` und führt einen Zähler `corrupted_pages`, der beim Einbinden gut sichtbar gemeldet wird.
|
||||||
|
- **Sekundärindex für Kindknoten (`children_index`, D-02):**
|
||||||
|
- `CarrierFsInner` pflegt einen `HashMap<i64, Vec<i64>>` In-Memory Index für Vater-Kind-Beziehungen.
|
||||||
|
- Reduziert Pfadauflösung (`resolve_path`) und Verzeichnis-Listings (`read_dir`) von $O(N)$ Tabellenscans auf $O(\text{Geschwister})$.
|
||||||
|
- **Transparente Migration & Allokations-Sicherheit (D-03, D-04, D-05):**
|
||||||
|
- V1-Container werden transparent geladen und beim ersten Schreibvorgang nach V2 migriert — inklusive Vorabprüfung, ob genügend freie Blöcke vorhanden sind.
|
||||||
|
- Freigabe überzähliger Seitenblöcke (`shred_carrier_block`) bei Inode-Löschungen.
|
||||||
|
- Ersetzung der statischen 80%-Füllstandswarnung durch eine Block-Verfügbarkeitswarnung bei `< 20` freien Blöcken oder `< 5%` Restkapazität.
|
||||||
|
- **Automatisierte Testabdeckung:** 7 neue Regressionstests in `tests/carrier_v2_paged_manifest_test.rs` decken Kapazität (>10.000 Inodes), Migration, Fail-Soft-Isolation, Sekundärindex-Konsistenz, Pfadauflösungs-Performance, Out-of-Space-Abbruch und Blockfreigabe ab (Gesamttests: 130/130 erfolgreich).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Verifikationsnachweis
|
## Verifikationsnachweis
|
||||||
|
|
||||||
Alle Unit- und Integrationstests wurden auf einem Windows x86_64 Host mit 100% Erfolgsquote ausgeführt. Die Release-Paketierung für Windows und Linux musl ist voll automatisiert und wird mit Minisign kryptografisch abgesichert.
|
Alle Unit- und Integrationstests wurden auf einem Windows x86_64 Host mit 100% Erfolgsquote ausgeführt (130 / 130 Tests). Die Release-Paketierung für Windows und Linux musl ist voll automatisiert und wird mit Minisign kryptografisch abgesichert.
|
||||||
+25
-1
@@ -1,6 +1,6 @@
|
|||||||
# Threat Model & Sicherheitsarchitektur von Sanctum
|
# Threat Model & Sicherheitsarchitektur von Sanctum
|
||||||
|
|
||||||
Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.8.0** im reinen Userland-Betrieb.
|
Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.9.0** im reinen Userland-Betrieb.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -102,3 +102,27 @@ Wird der Rechner abrupt stromlos gemacht (`Hard Reset`), stürzt das Betriebssys
|
|||||||
- **Datenintegrität**: Beim nächsten Öffnen des Containers führt SQLite automatisch ein WAL-Recovery durch, wodurch keine Daten verloren gehen und die Metadaten-Authentifizierung (K-01) intakt bleibt.
|
- **Datenintegrität**: Beim nächsten Öffnen des Containers führt SQLite automatisch ein WAL-Recovery durch, wodurch keine Daten verloren gehen und die Metadaten-Authentifizierung (K-01) intakt bleibt.
|
||||||
- **Forensische Sichtbarkeit**: Ein Angreifer kann aus der Existenz und Dateigröße der `-wal`-Datei ablesen, dass vor dem Absturz Schreiboperationen stattgefunden haben und wie viele Bytes modifiziert wurden.
|
- **Forensische Sichtbarkeit**: Ein Angreifer kann aus der Existenz und Dateigröße der `-wal`-Datei ablesen, dass vor dem Absturz Schreiboperationen stattgefunden haben und wie viele Bytes modifiziert wurden.
|
||||||
- **Flash-Wear-Leveling**: Auf SSDs/NVMe-Speichern können gelöschte Dateisystem-Sektoren bis zur TRIM-Bereinigung physisch im Flash-Speicher existieren. Für maximale OpSec empfiehlt Sanctum die Ablage von Containern auf vollverschlüsselten Host-Laufwerken (BitLocker / LUKS) oder RAM-Disks.
|
- **Flash-Wear-Leveling**: Auf SSDs/NVMe-Speichern können gelöschte Dateisystem-Sektoren bis zur TRIM-Bereinigung physisch im Flash-Speicher existieren. Für maximale OpSec empfiehlt Sanctum die Ablage von Containern auf vollverschlüsselten Host-Laufwerken (BitLocker / LUKS) oder RAM-Disks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Hidden Vault Carrier-Dateisystem & Redundanzmodell (Format V2)
|
||||||
|
|
||||||
|
### 6.1 Redundanzhierarchie: Superblock vs. Inode-Pages
|
||||||
|
Mit Sanctum v0.9.0 führt der Hidden Vault das Carrier-Format V2 (Paged Manifest) ein. Das Redundanzmodell unterscheidet bewusst zwischen Metadaten-Knotenpunkten und Inode-Seiten:
|
||||||
|
|
||||||
|
1. **Carrier-Superblock (Vollständige Redundanz auf Block 0 & Block 1)**:
|
||||||
|
- Der Superblock enthält die essenziellen Dateisystem-Parameter (`root_id`, `next_inode_id`, `free_blocks`, `page_block_indices`) sowie einen monotonen Generationszähler (`manifest_generation`) und CRC32-Prüfsummen.
|
||||||
|
- Er wird abwechselnd auf Block 0 und Block 1 geschrieben (rollierendes C-02-Schema). Ist einer der beiden Blöcke beschädigt oder unvollständig geschrieben, stellt Sanctum den Superblock transparent aus dem intakten Block wieder her.
|
||||||
|
2. **Inode-Pages (Dynamisch allokiert, nicht redundant dupliziert)**:
|
||||||
|
- Inodes werden in 1-MB-Seiten (`CarrierInodePage`, Magic `SANCTPAG`) im normalen Träger-Blockpool gespeichert.
|
||||||
|
- Zur Maximierung der nutzbaren Speicherkapazität werden Inode-Pages **nicht** blockweise gespiegelt. Stattdessen schützt Sanctum die Konsistenz über Fail-Soft-Isolation (D-01): Fällt eine einzelne Seite durch Bitrot oder Entschlüsselungsfehler aus, wird ausschließlich diese Seite übersprungen. Das restliche Dateisystem bleibt vollständig mountbar und lesbar.
|
||||||
|
- Erkannte Seitenbeschädigungen werden beim Mounten und in den Systemlogs (`corrupted_pages`) unübersehbar gemeldet.
|
||||||
|
|
||||||
|
### 6.2 Pfadauflösungs- und DoS-Schutz (`children_index`, D-02)
|
||||||
|
- Bei sehr großen Dateisystemen (>10.000 Inodes) führt eine lineare Suche über alle Inodes bei jedem Pfadsegment zu quadratischer Laufzeitkomplexität ($O(N)$ pro Segment).
|
||||||
|
- Sanctum v0.9.0 hält einen In-Memory-Sekundärindex (`children_index: HashMap<i64, Vec<i64>>`), der Pfadauflösungen auf $O(\text{Geschwister})$ reduziert. Dies verhindert CPU-Erschöpfungs-Angriffe (ReDoS/Algorithmic Complexity Attacks) beim Traversieren tiefer Verzeichnisstrukturen.
|
||||||
|
|
||||||
|
### 6.3 Speicherallokationsgrenzen & Transaktionssicherheit (D-03, D-05)
|
||||||
|
- **Vorabprüfung bei Migration (D-03)**: Bei der transparenten Konvertierung alter V1-Manifeste nach V2 prüft Sanctum vorab, ob ausreichend freie Trägerblöcke vorhanden sind. Reicht der Speicherplatz nicht aus, wird der Speichervorgang ohne Beschädigung des V1-Containers abgebrochen.
|
||||||
|
- **Kryptografische Blockfreigabe (D-05)**: Werden Inodes gelöscht und Inode-Pages überflüssig, werden die freigegebenen Trägerblöcke sofort mit CSPRNG-Rauschen überschrieben (`shred_carrier_block`), bevor sie an den Freispeicher-Pool zurückgegeben werden.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.8.1",
|
"version": "0.9.0",
|
||||||
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
||||||
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"architecture": {
|
"architecture": {
|
||||||
"64bit": {
|
"64bit": {
|
||||||
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.8.1/sanctum-v0.8.1-windows-x86_64.zip",
|
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.0/sanctum-v0.9.0-windows-x86_64.zip",
|
||||||
"hash": "11f06952f743d625c1cda627df685064a91cd973e52b17e7bcaf96385bb4969c",
|
"hash": "47e26713880b812a332f7ee09737ace2ca3d7471d6375c0febe2d9b040ee8b43",
|
||||||
"bin": "sanctum.exe"
|
"bin": "sanctum.exe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
||||||
PackageIdentifier: HaraldPansi.Sanctum
|
PackageIdentifier: HaraldPansi.Sanctum
|
||||||
PackageVersion: 0.8.1
|
PackageVersion: 0.9.0
|
||||||
PackageName: Sanctum
|
PackageName: Sanctum
|
||||||
Publisher: Harald Pansi
|
Publisher: Harald Pansi
|
||||||
PublisherUrl: https://gitea.pansi.eu/harald
|
PublisherUrl: https://gitea.pansi.eu/harald
|
||||||
@@ -18,7 +18,7 @@ Tags:
|
|||||||
- webdav
|
- webdav
|
||||||
- container
|
- container
|
||||||
- plausible-deniability
|
- plausible-deniability
|
||||||
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.8.1
|
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.0
|
||||||
Installers:
|
Installers:
|
||||||
- Architecture: x64
|
- Architecture: x64
|
||||||
InstallerType: zip
|
InstallerType: zip
|
||||||
@@ -26,7 +26,7 @@ Installers:
|
|||||||
NestedInstallerFiles:
|
NestedInstallerFiles:
|
||||||
- RelativeFilePath: sanctum.exe
|
- RelativeFilePath: sanctum.exe
|
||||||
PortableCommandAlias: sanctum
|
PortableCommandAlias: sanctum
|
||||||
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.8.1/sanctum-v0.8.1-windows-x86_64.zip
|
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.0/sanctum-v0.9.0-windows-x86_64.zip
|
||||||
InstallerSha256: 11f06952f743d625c1cda627df685064a91cd973e52b17e7bcaf96385bb4969c
|
InstallerSha256: 47e26713880b812a332f7ee09737ace2ca3d7471d6375c0febe2d9b040ee8b43
|
||||||
ManifestType: singleton
|
ManifestType: singleton
|
||||||
ManifestVersion: 1.6.0
|
ManifestVersion: 1.6.0
|
||||||
|
|||||||
+444
-106
@@ -27,10 +27,40 @@ use crate::vfs::{is_leak_file_with_custom, SanctumDirEntry, SanctumMetaData};
|
|||||||
|
|
||||||
pub const CARRIER_MAGIC: &[u8; 8] = b"SANCTCAR";
|
pub const CARRIER_MAGIC: &[u8; 8] = b"SANCTCAR";
|
||||||
pub const CARRIER_VERSION: u32 = 1;
|
pub const CARRIER_VERSION: u32 = 1;
|
||||||
|
pub const CARRIER_VERSION_V2: u32 = 2;
|
||||||
|
pub const CARRIER_PAGE_MAGIC: &[u8; 8] = b"SANCTPAG";
|
||||||
pub const CARRIER_MANIFEST_CAPACITY: usize = CHUNK_SIZE - 64; // 1,048,512 Bytes
|
pub const CARRIER_MANIFEST_CAPACITY: usize = CHUNK_SIZE - 64; // 1,048,512 Bytes
|
||||||
pub const CARRIER_MANIFEST_WARN_THRESHOLD: usize = CARRIER_MANIFEST_CAPACITY * 8 / 10; // 838,809 Bytes (80%, C-04)
|
pub const CARRIER_MANIFEST_WARN_THRESHOLD: usize = CARRIER_MANIFEST_CAPACITY * 8 / 10; // 838,809 Bytes (80%, C-04)
|
||||||
|
|
||||||
/// Manifest für das steganografische Dateisystem innerhalb des Alibi-Carriers (Block 0 und 1, C-02).
|
/// Superblock: globaler Zustand, redundant auf Block 0 und 1 (C-02, Format V2).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CarrierSuperblock {
|
||||||
|
pub magic: [u8; 8],
|
||||||
|
pub version: u32,
|
||||||
|
pub manifest_generation: u64,
|
||||||
|
pub total_blocks: u32,
|
||||||
|
pub free_blocks: Vec<u32>,
|
||||||
|
pub next_inode_id: i64,
|
||||||
|
pub page_block_indices: Vec<u32>, // Reihenfolge = Seitenreihenfolge
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine Inode-Tabellenseite (~1 MB Budget, serialisiert in einen Carrier-Block).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CarrierInodePage {
|
||||||
|
pub magic: [u8; 8], // eigenes Sub-Magic b"SANCTPAG"
|
||||||
|
pub entries: Vec<CarrierInode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hilfsstruktur zur Typerkennung (V1 vs V2) in Block 0/1.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VersionProbe {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub magic: [u8; 8],
|
||||||
|
pub version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In-Memory-Zustand des Carrier-Manifests.
|
||||||
|
/// Hält Superblock-Felder und die vollständige Inode-Map für schnellen Zugriff.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CarrierManifest {
|
pub struct CarrierManifest {
|
||||||
pub magic: [u8; 8],
|
pub magic: [u8; 8],
|
||||||
@@ -40,6 +70,9 @@ pub struct CarrierManifest {
|
|||||||
pub total_blocks: u32,
|
pub total_blocks: u32,
|
||||||
pub free_blocks: Vec<u32>,
|
pub free_blocks: Vec<u32>,
|
||||||
pub next_inode_id: i64,
|
pub next_inode_id: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub page_block_indices: Vec<u32>,
|
||||||
|
#[serde(default)]
|
||||||
pub inodes: HashMap<i64, CarrierInode>,
|
pub inodes: HashMap<i64, CarrierInode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +99,28 @@ impl CarrierManifest {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Block 0 und Block 1 sind für das redundante Manifest reserviert (C-02). Nutzblöcke sind 2..total_blocks-1.
|
// Block 0 und Block 1 sind für den Superblock reserviert (C-02).
|
||||||
let free_blocks = (2..total_blocks).collect();
|
// Block 2 wird für die erste Inode-Tabellenseite allokiert (falls total_blocks > 2).
|
||||||
|
// Nutzblöcke starten ab Block 3.
|
||||||
|
let page_block_indices = if total_blocks > 2 {
|
||||||
|
vec![2]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
let free_blocks = if total_blocks > 3 {
|
||||||
|
(3..total_blocks).collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
magic: *CARRIER_MAGIC,
|
magic: *CARRIER_MAGIC,
|
||||||
version: CARRIER_VERSION,
|
version: CARRIER_VERSION_V2,
|
||||||
manifest_generation: 0,
|
manifest_generation: 0,
|
||||||
total_blocks,
|
total_blocks,
|
||||||
free_blocks,
|
free_blocks,
|
||||||
next_inode_id: 2,
|
next_inode_id: 2,
|
||||||
|
page_block_indices,
|
||||||
inodes,
|
inodes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,6 +324,8 @@ pub struct CarrierFsInner {
|
|||||||
pub custom_leak_rules: Arc<Vec<String>>,
|
pub custom_leak_rules: Arc<Vec<String>>,
|
||||||
pub leak_counter: Arc<AtomicU64>,
|
pub leak_counter: Arc<AtomicU64>,
|
||||||
pub manifest: CarrierManifest,
|
pub manifest: CarrierManifest,
|
||||||
|
pub children_index: HashMap<i64, Vec<i64>>, // parent_id -> [child_id, ...] (D-02)
|
||||||
|
pub corrupted_pages: usize, // Zähler für übersprungene/beschädigte Seiten (D-01)
|
||||||
pub manifest_dirty: bool,
|
pub manifest_dirty: bool,
|
||||||
pub manifest_save_count: u64,
|
pub manifest_save_count: u64,
|
||||||
pub dirty_ops_count: u32,
|
pub dirty_ops_count: u32,
|
||||||
@@ -291,13 +338,31 @@ impl CarrierFsInner {
|
|||||||
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Baut den Sekundärindex (parent_id -> [child_id, ...]) aus der Inode-Map auf (D-02).
|
||||||
|
pub fn build_children_index(inodes: &HashMap<i64, CarrierInode>) -> HashMap<i64, Vec<i64>> {
|
||||||
|
let mut index: HashMap<i64, Vec<i64>> = HashMap::new();
|
||||||
|
for (&id, inode) in inodes {
|
||||||
|
if let Some(parent_id) = inode.parent_id {
|
||||||
|
index.entry(parent_id).or_default().push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_add_child(&mut self, parent_id: i64, child_id: i64) {
|
||||||
|
let entry = self.children_index.entry(parent_id).or_default();
|
||||||
|
if !entry.contains(&child_id) {
|
||||||
|
entry.push(child_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_remove_child(&mut self, parent_id: i64, child_id: i64) {
|
||||||
|
if let Some(children) = self.children_index.get_mut(&parent_id) {
|
||||||
|
children.retain(|&id| id != child_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Markiert das Manifest als modifiziert (dirty) ohne teure synchrone Neuverschlüsselung (C-03).
|
/// Markiert das Manifest als modifiziert (dirty) ohne teure synchrone Neuverschlüsselung (C-03).
|
||||||
/// Erst bei flush(), close(), unmount/drop oder nach Überschreiten von 50 Operationen wird
|
|
||||||
/// das vollständige 1-MB-Manifest tatsächlich neu verschlüsselt und geschrieben.
|
|
||||||
///
|
|
||||||
/// Konsistenzhinweis:
|
|
||||||
/// Bei einem abrupten Prozessabbruch gilt der zuletzt gespeicherte Zustand. Dank C-02
|
|
||||||
/// (rollierendes Manifest auf Block 0 und Block 1) ist die Integrität stets gewährleistet.
|
|
||||||
pub fn mark_dirty(&mut self) {
|
pub fn mark_dirty(&mut self) {
|
||||||
self.manifest_dirty = true;
|
self.manifest_dirty = true;
|
||||||
self.dirty_ops_count += 1;
|
self.dirty_ops_count += 1;
|
||||||
@@ -314,15 +379,37 @@ impl CarrierFsInner {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ermittelt die aktuelle Bytegröße des Manifests, die Kapazität und den Füllstand in Prozent (C-04).
|
/// Ermittelt die aktuelle Bytegröße des Manifests, die Kapazität und den Füllstand in Prozent.
|
||||||
pub fn manifest_usage(&self) -> (usize, usize, f64) {
|
pub fn manifest_usage(&self) -> (usize, usize, f64) {
|
||||||
let len = serde_json::to_vec(&self.manifest)
|
let pages_count = self.manifest.page_block_indices.len().max(1);
|
||||||
.map(|v| v.len())
|
let total_page_capacity = pages_count * CARRIER_MANIFEST_CAPACITY;
|
||||||
.unwrap_or(0);
|
let mut total_bytes = 0usize;
|
||||||
let pct = (len as f64 / CARRIER_MANIFEST_CAPACITY as f64) * 100.0;
|
for inode in self.manifest.inodes.values() {
|
||||||
(len, CARRIER_MANIFEST_CAPACITY, pct)
|
total_bytes += serde_json::to_vec(inode)
|
||||||
|
.map(|v| v.len() + 2)
|
||||||
|
.unwrap_or(200);
|
||||||
|
}
|
||||||
|
let pct = if total_page_capacity > 0 {
|
||||||
|
(total_bytes as f64 / total_page_capacity as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
(total_bytes, total_page_capacity, pct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn free_blocks_count(&self) -> usize {
|
||||||
|
self.manifest.free_blocks.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_blocks_count(&self) -> u32 {
|
||||||
|
self.manifest.total_blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page_count(&self) -> usize {
|
||||||
|
self.manifest.page_block_indices.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Speichert das Carrier-Manifest im Format V2 (Superblock + Inode-Tabellenseiten, D-04).
|
||||||
pub fn save_manifest(&mut self) -> Result<()> {
|
pub fn save_manifest(&mut self) -> Result<()> {
|
||||||
self.manifest_save_count += 1;
|
self.manifest_save_count += 1;
|
||||||
self.manifest_dirty = false;
|
self.manifest_dirty = false;
|
||||||
@@ -332,26 +419,119 @@ impl CarrierFsInner {
|
|||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
self.manifest.manifest_generation += 1;
|
self.manifest.manifest_generation += 1;
|
||||||
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
|
||||||
if manifest_bytes.len() > CARRIER_MANIFEST_CAPACITY {
|
// D-04: Inodes auf Seiten aufteilen (deterministisch nach Inode-ID sortiert)
|
||||||
bail!(
|
let mut sorted_inodes: Vec<CarrierInode> = self.manifest.inodes.values().cloned().collect();
|
||||||
"Carrier-Manifest überschreitet die maximale Blockgröße ({} > {} Bytes). Empfohlene Obergrenze ca. 7.000 Dateien/Verzeichnisse beachten.",
|
sorted_inodes.sort_by_key(|i| i.id);
|
||||||
manifest_bytes.len(),
|
|
||||||
CARRIER_MANIFEST_CAPACITY
|
let mut pages: Vec<CarrierInodePage> = Vec::new();
|
||||||
);
|
let mut current_entries = Vec::new();
|
||||||
|
let mut current_bytes = 64usize; // Header-Overhead für CarrierInodePage JSON
|
||||||
|
|
||||||
|
for inode in sorted_inodes {
|
||||||
|
let entry_len = serde_json::to_vec(&inode)
|
||||||
|
.map(|v| v.len() + 2)
|
||||||
|
.unwrap_or(256);
|
||||||
|
if current_bytes + entry_len > CARRIER_MANIFEST_CAPACITY && !current_entries.is_empty()
|
||||||
|
{
|
||||||
|
pages.push(CarrierInodePage {
|
||||||
|
magic: *CARRIER_PAGE_MAGIC,
|
||||||
|
entries: std::mem::take(&mut current_entries),
|
||||||
|
});
|
||||||
|
current_bytes = 64;
|
||||||
|
}
|
||||||
|
current_bytes += entry_len;
|
||||||
|
current_entries.push(inode);
|
||||||
}
|
}
|
||||||
if manifest_bytes.len() >= CARRIER_MANIFEST_WARN_THRESHOLD {
|
if !current_entries.is_empty() || pages.is_empty() {
|
||||||
let pct = (manifest_bytes.len() * 100) / CARRIER_MANIFEST_CAPACITY;
|
pages.push(CarrierInodePage {
|
||||||
warn!(
|
magic: *CARRIER_PAGE_MAGIC,
|
||||||
"Warnung (C-04): Hidden-Vault Manifest zu {}% gefüllt ({}/{} Bytes). Empfohlene Obergrenze beachten (~7.000 Dateien/Verzeichnisse).",
|
entries: current_entries,
|
||||||
pct,
|
});
|
||||||
manifest_bytes.len(),
|
}
|
||||||
|
|
||||||
|
let needed_pages = pages.len();
|
||||||
|
let old_page_blocks = self.manifest.page_block_indices.clone();
|
||||||
|
let old_page_count = old_page_blocks.len();
|
||||||
|
|
||||||
|
let mut new_page_blocks = old_page_blocks;
|
||||||
|
|
||||||
|
if needed_pages > old_page_count {
|
||||||
|
let additional_needed = needed_pages - old_page_count;
|
||||||
|
if self.manifest.free_blocks.len() < additional_needed {
|
||||||
|
bail!(
|
||||||
|
"Nicht genügend freie Blöcke für die Inode-Tabelle (benötigt: {}, frei: {}). Bitte Speicherplatz freigeben oder Container vergrößern.",
|
||||||
|
additional_needed,
|
||||||
|
self.manifest.free_blocks.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for _ in 0..additional_needed {
|
||||||
|
let block = self.manifest.free_blocks.pop().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Keine freien Blöcke mehr für Inode-Seitenallokation")
|
||||||
|
})?;
|
||||||
|
new_page_blocks.push(block);
|
||||||
|
}
|
||||||
|
} else if needed_pages < old_page_count {
|
||||||
|
// D-05: Überzählige Seitenblöcke freigeben und schreddern
|
||||||
|
let surplus = new_page_blocks.split_off(needed_pages);
|
||||||
|
for b in surplus {
|
||||||
|
let _ = shred_carrier_block(
|
||||||
|
&self.db,
|
||||||
|
self.carrier_node_id,
|
||||||
|
b,
|
||||||
|
&self.dek_outer,
|
||||||
|
self.format_version,
|
||||||
|
);
|
||||||
|
self.manifest.free_blocks.push(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Inode-Tabellenseiten schreiben
|
||||||
|
for (page_idx, page) in pages.iter().enumerate() {
|
||||||
|
let block_idx = new_page_blocks[page_idx];
|
||||||
|
let page_bytes = serde_json::to_vec(page)?;
|
||||||
|
if page_bytes.len() > CARRIER_MANIFEST_CAPACITY {
|
||||||
|
bail!(
|
||||||
|
"Inode-Tabellenseite #{} überschreitet Blockgröße ({} > {} Bytes)",
|
||||||
|
page_idx,
|
||||||
|
page_bytes.len(),
|
||||||
|
CARRIER_MANIFEST_CAPACITY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
write_carrier_block(
|
||||||
|
&self.db,
|
||||||
|
self.carrier_node_id,
|
||||||
|
block_idx,
|
||||||
|
&self.dek_outer,
|
||||||
|
&self.dek_inner,
|
||||||
|
&page_bytes,
|
||||||
|
self.format_version,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.manifest.page_block_indices = new_page_blocks.clone();
|
||||||
|
self.manifest.version = CARRIER_VERSION_V2;
|
||||||
|
|
||||||
|
// Superblock schreiben (C-02 Dual-Block Rolling Scheme)
|
||||||
|
let superblock = CarrierSuperblock {
|
||||||
|
magic: *CARRIER_MAGIC,
|
||||||
|
version: CARRIER_VERSION_V2,
|
||||||
|
manifest_generation: self.manifest.manifest_generation,
|
||||||
|
total_blocks: self.manifest.total_blocks,
|
||||||
|
free_blocks: self.manifest.free_blocks.clone(),
|
||||||
|
next_inode_id: self.manifest.next_inode_id,
|
||||||
|
page_block_indices: new_page_blocks,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sb_bytes = serde_json::to_vec(&superblock)?;
|
||||||
|
if sb_bytes.len() > CARRIER_MANIFEST_CAPACITY {
|
||||||
|
bail!(
|
||||||
|
"Superblock überschreitet Blockgröße ({} > {} Bytes)",
|
||||||
|
sb_bytes.len(),
|
||||||
CARRIER_MANIFEST_CAPACITY
|
CARRIER_MANIFEST_CAPACITY
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// C-02: Rollierendes Dual-Block-Manifest.
|
|
||||||
// Gerade Generation -> Block 0, ungerade Generation -> Block 1.
|
|
||||||
let target_block = if self.manifest.manifest_generation.is_multiple_of(2) {
|
let target_block = if self.manifest.manifest_generation.is_multiple_of(2) {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
@@ -364,9 +544,20 @@ impl CarrierFsInner {
|
|||||||
target_block,
|
target_block,
|
||||||
&self.dek_outer,
|
&self.dek_outer,
|
||||||
&self.dek_inner,
|
&self.dek_inner,
|
||||||
&manifest_bytes,
|
&sb_bytes,
|
||||||
self.format_version,
|
self.format_version,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Warnung bei knappem freiem Blockpool
|
||||||
|
let free_len = self.manifest.free_blocks.len();
|
||||||
|
let total = self.manifest.total_blocks;
|
||||||
|
if total > 0 && (free_len < 20 || (free_len as f64 / total as f64) < 0.05) {
|
||||||
|
warn!(
|
||||||
|
"Warnung: Hidden-Vault hat nur noch {} freie Blöcke (von {}).",
|
||||||
|
free_len, total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +580,7 @@ impl CarrierFsInner {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Löst einen Pfad anhand des Sekundärindex auf (D-02, O(Kindknoten) statt O(alle Inodes)).
|
||||||
pub fn resolve_path(&self, raw_path: &str) -> Option<CarrierInode> {
|
pub fn resolve_path(&self, raw_path: &str) -> Option<CarrierInode> {
|
||||||
let trimmed = raw_path.trim_matches('/');
|
let trimmed = raw_path.trim_matches('/');
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -399,10 +591,17 @@ impl CarrierFsInner {
|
|||||||
let mut current_id = 1i64;
|
let mut current_id = 1i64;
|
||||||
|
|
||||||
for (idx, segment) in segments.iter().enumerate() {
|
for (idx, segment) in segments.iter().enumerate() {
|
||||||
let child = self.manifest.inodes.values().find(|inode| {
|
let child_ids = self.children_index.get(¤t_id)?;
|
||||||
inode.parent_id == Some(current_id) && inode.name.as_str() == *segment
|
let child_id = child_ids.iter().find(|&&cid| {
|
||||||
|
self.manifest
|
||||||
|
.inodes
|
||||||
|
.get(&cid)
|
||||||
|
.map(|inode| inode.name.as_str() == *segment)
|
||||||
|
.unwrap_or(false)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let child = self.manifest.inodes.get(child_id)?;
|
||||||
|
|
||||||
if idx + 1 < segments.len() && !child.is_dir {
|
if idx + 1 < segments.len() && !child.is_dir {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -436,14 +635,29 @@ impl Drop for CarrierFsInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LoadedBlock {
|
||||||
|
V1(CarrierManifest),
|
||||||
|
V2(CarrierSuperblock),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadedBlock {
|
||||||
|
fn generation(&self) -> u64 {
|
||||||
|
match self {
|
||||||
|
LoadedBlock::V1(m) => m.manifest_generation,
|
||||||
|
LoadedBlock::V2(sb) => sb.manifest_generation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// WebDAV-Filesystem-Treiber für den steganografischen Alibi-Carrier (Hidden Vault).
|
/// WebDAV-Filesystem-Treiber für den steganografischen Alibi-Carrier (Hidden Vault).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CarrierFs {
|
pub struct CarrierFs {
|
||||||
inner: Arc<Mutex<CarrierFsInner>>,
|
pub inner: Arc<Mutex<CarrierFsInner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CarrierFs {
|
impl CarrierFs {
|
||||||
/// Lädt ein bestehendes Carrier-Dateisystem mit redundanter Block-0/Block-1 Prüfung (C-02)
|
/// Lädt ein bestehendes Carrier-Dateisystem mit redundanter Superblock-Prüfung (C-02),
|
||||||
|
/// seitenbasierter Inode-Tabelle (D-01), V1->V2 Migrationsunterstützung (D-03)
|
||||||
/// und benutzerdefinierten Anti-Leak-Regeln (V-04).
|
/// und benutzerdefinierten Anti-Leak-Regeln (V-04).
|
||||||
pub fn load_with_leak_rules(
|
pub fn load_with_leak_rules(
|
||||||
db: Database,
|
db: Database,
|
||||||
@@ -454,68 +668,62 @@ impl CarrierFs {
|
|||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
custom_leak_rules: Vec<String>,
|
custom_leak_rules: Vec<String>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// C-02: Versuche beide redundante Blöcke (Block 0 und Block 1) zu lesen und zu verifizieren
|
let read_and_parse_block = |block_idx: u32| -> Result<LoadedBlock> {
|
||||||
let block_0_res = read_carrier_block(
|
let bytes = read_carrier_block(
|
||||||
&db,
|
&db,
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
0,
|
block_idx,
|
||||||
&dek_outer,
|
&dek_outer,
|
||||||
&dek_inner,
|
&dek_inner,
|
||||||
format_version,
|
format_version,
|
||||||
)
|
)?;
|
||||||
.and_then(|bytes| {
|
let probe: VersionProbe = serde_json::from_slice(&bytes)?;
|
||||||
let m: CarrierManifest = serde_json::from_slice(&bytes)?;
|
if probe.magic != *CARRIER_MAGIC {
|
||||||
if m.magic != *CARRIER_MAGIC {
|
bail!("Ungültige Carrier-Magic-Bytes in Block {block_idx}");
|
||||||
bail!("Ungültige Carrier-Magic-Bytes in Block 0");
|
|
||||||
}
|
}
|
||||||
Ok(m)
|
if probe.version == CARRIER_VERSION {
|
||||||
});
|
let m: CarrierManifest = serde_json::from_slice(&bytes)?;
|
||||||
|
Ok(LoadedBlock::V1(m))
|
||||||
let block_1_res = read_carrier_block(
|
} else if probe.version == CARRIER_VERSION_V2 {
|
||||||
&db,
|
let sb: CarrierSuperblock = serde_json::from_slice(&bytes)?;
|
||||||
carrier_node_id,
|
Ok(LoadedBlock::V2(sb))
|
||||||
1,
|
} else {
|
||||||
&dek_outer,
|
bail!("Unbekannte Carrier-Formatversion: {}", probe.version);
|
||||||
&dek_inner,
|
|
||||||
format_version,
|
|
||||||
)
|
|
||||||
.and_then(|bytes| {
|
|
||||||
let m: CarrierManifest = serde_json::from_slice(&bytes)?;
|
|
||||||
if m.magic != *CARRIER_MAGIC {
|
|
||||||
bail!("Ungültige Carrier-Magic-Bytes in Block 1");
|
|
||||||
}
|
}
|
||||||
Ok(m)
|
};
|
||||||
});
|
|
||||||
|
|
||||||
let mut manifest = match (block_0_res, block_1_res) {
|
let block_0_res = read_and_parse_block(0);
|
||||||
(Ok(m0), Ok(m1)) => {
|
let block_1_res = read_and_parse_block(1);
|
||||||
if m1.manifest_generation > m0.manifest_generation {
|
|
||||||
|
let loaded_block = match (block_0_res, block_1_res) {
|
||||||
|
(Ok(b0), Ok(b1)) => {
|
||||||
|
if b1.generation() > b0.generation() {
|
||||||
debug!(
|
debug!(
|
||||||
"C-02: Beide Manifest-Kopien intakt. Wähle neuere Generation {} aus Block 1 (Block 0: Gen {})",
|
"C-02: Beide Superblock-Kopien intakt. Wähle neuere Generation {} aus Block 1 (Block 0: Gen {})",
|
||||||
m1.manifest_generation, m0.manifest_generation
|
b1.generation(), b0.generation()
|
||||||
);
|
);
|
||||||
m1
|
b1
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"C-02: Beide Manifest-Kopien intakt. Wähle Generation {} aus Block 0 (Block 1: Gen {})",
|
"C-02: Beide Superblock-Kopien intakt. Wähle Generation {} aus Block 0 (Block 1: Gen {})",
|
||||||
m0.manifest_generation, m1.manifest_generation
|
b0.generation(), b1.generation()
|
||||||
);
|
);
|
||||||
m0
|
b0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Ok(m0), Err(e1)) => {
|
(Ok(b0), Err(e1)) => {
|
||||||
debug!(
|
debug!(
|
||||||
"C-02: Verwende primäres Manifest aus Block 0 (Gen {}). Block 1 unlesbar oder uninitialisiert: {}",
|
"C-02: Verwende primären Superblock aus Block 0 (Gen {}). Block 1 unlesbar oder uninitialisiert: {}",
|
||||||
m0.manifest_generation, e1
|
b0.generation(), e1
|
||||||
);
|
);
|
||||||
m0
|
b0
|
||||||
}
|
}
|
||||||
(Err(e0), Ok(m1)) => {
|
(Err(e0), Ok(b1)) => {
|
||||||
warn!(
|
warn!(
|
||||||
"C-02: Block 0 korrupt oder unlesbar ({})! Erfolgreiche Wiederherstellung aus redundanter Kopie in Block 1 (Gen {}).",
|
"C-02: Block 0 korrupt oder unlesbar ({})! Erfolgreiche Wiederherstellung aus redundanter Kopie in Block 1 (Gen {}).",
|
||||||
e0, m1.manifest_generation
|
e0, b1.generation()
|
||||||
);
|
);
|
||||||
m1
|
b1
|
||||||
}
|
}
|
||||||
(Err(e0), Err(e1)) => {
|
(Err(e0), Err(e1)) => {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -525,20 +733,101 @@ impl CarrierFs {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Manifest-Kopie reservieren
|
let mut corrupted_pages = 0usize;
|
||||||
manifest.free_blocks.retain(|&b| b >= 2);
|
let manifest = match loaded_block {
|
||||||
|
LoadedBlock::V1(mut m) => {
|
||||||
// C-04: Warnung bei Füllstand über 80% beim Mounten
|
// Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Kopie reservieren
|
||||||
if let Ok(manifest_bytes) = serde_json::to_vec(&manifest) {
|
m.free_blocks.retain(|&b| b >= 2);
|
||||||
if manifest_bytes.len() >= CARRIER_MANIFEST_WARN_THRESHOLD {
|
m
|
||||||
let pct = (manifest_bytes.len() * 100) / CARRIER_MANIFEST_CAPACITY;
|
|
||||||
warn!(
|
|
||||||
"Warnung (C-04): Hidden-Vault Manifest zu {}% gefüllt ({}/{} Bytes). Empfohlene Obergrenze beachten (~7.000 Dateien/Verzeichnisse).",
|
|
||||||
pct,
|
|
||||||
manifest_bytes.len(),
|
|
||||||
CARRIER_MANIFEST_CAPACITY
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
LoadedBlock::V2(sb) => {
|
||||||
|
let mut inodes = HashMap::new();
|
||||||
|
for &page_block_idx in &sb.page_block_indices {
|
||||||
|
match read_carrier_block(
|
||||||
|
&db,
|
||||||
|
carrier_node_id,
|
||||||
|
page_block_idx,
|
||||||
|
&dek_outer,
|
||||||
|
&dek_inner,
|
||||||
|
format_version,
|
||||||
|
) {
|
||||||
|
Ok(page_bytes) => {
|
||||||
|
match serde_json::from_slice::<CarrierInodePage>(&page_bytes) {
|
||||||
|
Ok(page) => {
|
||||||
|
if page.magic != *CARRIER_PAGE_MAGIC {
|
||||||
|
error!(
|
||||||
|
"D-01: Ungültige Magic in Inode-Tabellenseite auf Block {} (erwartet SANCTPAG)",
|
||||||
|
page_block_idx
|
||||||
|
);
|
||||||
|
corrupted_pages += 1;
|
||||||
|
} else {
|
||||||
|
for entry in page.entries {
|
||||||
|
inodes.insert(entry.id, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"D-01: Deserialisierungsfehler in Inode-Tabellenseite auf Block {}: {}",
|
||||||
|
page_block_idx, e
|
||||||
|
);
|
||||||
|
corrupted_pages += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"D-01: Lesefehler auf Inode-Tabellenseite Block {}: {}",
|
||||||
|
page_block_idx, e
|
||||||
|
);
|
||||||
|
corrupted_pages += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sicherstellen, dass das Wurzelverzeichnis '/' (Inode 1) vorhanden ist
|
||||||
|
inodes.entry(1).or_insert_with(|| {
|
||||||
|
warn!("Wurzelknoten (Inode 1) fehlt in geladenen Seiten, wird rekonstruiert.");
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
CarrierInode {
|
||||||
|
id: 1,
|
||||||
|
parent_id: None,
|
||||||
|
name: String::new(),
|
||||||
|
is_dir: true,
|
||||||
|
size: 0,
|
||||||
|
created_at: now,
|
||||||
|
modified_at: now,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CarrierManifest {
|
||||||
|
magic: sb.magic,
|
||||||
|
version: sb.version,
|
||||||
|
manifest_generation: sb.manifest_generation,
|
||||||
|
total_blocks: sb.total_blocks,
|
||||||
|
free_blocks: sb.free_blocks,
|
||||||
|
next_inode_id: sb.next_inode_id,
|
||||||
|
page_block_indices: sb.page_block_indices,
|
||||||
|
inodes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// D-02: Sekundärindex aufbauen
|
||||||
|
let children_index = CarrierFsInner::build_children_index(&manifest.inodes);
|
||||||
|
|
||||||
|
// Warnung bei knappem freiem Blockpool
|
||||||
|
let free_len = manifest.free_blocks.len();
|
||||||
|
let total = manifest.total_blocks;
|
||||||
|
if total > 0 && (free_len < 20 || (free_len as f64 / total as f64) < 0.05) {
|
||||||
|
warn!(
|
||||||
|
"Warnung: Hidden-Vault hat nur noch {} freie Blöcke (von {}).",
|
||||||
|
free_len, total
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
@@ -560,6 +849,8 @@ impl CarrierFs {
|
|||||||
custom_leak_rules: Arc::new(custom_leak_rules),
|
custom_leak_rules: Arc::new(custom_leak_rules),
|
||||||
leak_counter: Arc::new(AtomicU64::new(0)),
|
leak_counter: Arc::new(AtomicU64::new(0)),
|
||||||
manifest,
|
manifest,
|
||||||
|
children_index,
|
||||||
|
corrupted_pages,
|
||||||
manifest_dirty: false,
|
manifest_dirty: false,
|
||||||
manifest_save_count: 0,
|
manifest_save_count: 0,
|
||||||
dirty_ops_count: 0,
|
dirty_ops_count: 0,
|
||||||
@@ -612,6 +903,26 @@ impl CarrierFs {
|
|||||||
inner.manifest_usage()
|
inner.manifest_usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn corrupted_pages(&self) -> usize {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.corrupted_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block_stats(&self) -> (usize, u32) {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
(inner.free_blocks_count(), inner.total_blocks_count())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page_count(&self) -> usize {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.page_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_path(&self, raw_path: &str) -> Option<CarrierInode> {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.resolve_path(raw_path)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn leak_counter(&self) -> Arc<AtomicU64> {
|
pub fn leak_counter(&self) -> Arc<AtomicU64> {
|
||||||
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
inner.leak_counter.clone()
|
inner.leak_counter.clone()
|
||||||
@@ -721,6 +1032,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_inode.clone());
|
inner.manifest.inodes.insert(new_id, new_inode.clone());
|
||||||
|
inner.index_add_child(parent.id, new_id);
|
||||||
inner.mark_dirty();
|
inner.mark_dirty();
|
||||||
new_inode
|
new_inode
|
||||||
} else {
|
} else {
|
||||||
@@ -750,11 +1062,14 @@ impl DavFileSystem for CarrierFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = inner
|
let child_ids = inner
|
||||||
.manifest
|
.children_index
|
||||||
.inodes
|
.get(&node.id)
|
||||||
.values()
|
.cloned()
|
||||||
.filter(|child| child.parent_id == Some(node.id))
|
.unwrap_or_default();
|
||||||
|
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = child_ids
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|cid| inner.manifest.inodes.get(&cid))
|
||||||
.filter(|child| {
|
.filter(|child| {
|
||||||
if inner.is_leak(&child.name) {
|
if inner.is_leak(&child.name) {
|
||||||
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
@@ -843,6 +1158,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_dir);
|
inner.manifest.inodes.insert(new_id, new_dir);
|
||||||
|
inner.index_add_child(parent.id, new_id);
|
||||||
inner.mark_dirty();
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -864,17 +1180,20 @@ impl DavFileSystem for CarrierFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe, ob das Verzeichnis leer ist
|
// Prüfe, ob das Verzeichnis leer ist (via Sekundärindex)
|
||||||
let has_children = inner
|
let has_children = inner
|
||||||
.manifest
|
.children_index
|
||||||
.inodes
|
.get(&node.id)
|
||||||
.values()
|
.is_some_and(|children| !children.is_empty());
|
||||||
.any(|child| child.parent_id == Some(node.id));
|
|
||||||
|
|
||||||
if has_children {
|
if has_children {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(p_id) = node.parent_id {
|
||||||
|
inner.index_remove_child(p_id, node.id);
|
||||||
|
}
|
||||||
|
inner.children_index.remove(&node.id);
|
||||||
inner.manifest.inodes.remove(&node.id);
|
inner.manifest.inodes.remove(&node.id);
|
||||||
inner.mark_dirty();
|
inner.mark_dirty();
|
||||||
|
|
||||||
@@ -904,6 +1223,9 @@ impl DavFileSystem for CarrierFs {
|
|||||||
inner.manifest.free_blocks.push(b);
|
inner.manifest.free_blocks.push(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(p_id) = node.parent_id {
|
||||||
|
inner.index_remove_child(p_id, node.id);
|
||||||
|
}
|
||||||
inner.manifest.inodes.remove(&node.id);
|
inner.manifest.inodes.remove(&node.id);
|
||||||
inner.mark_dirty();
|
inner.mark_dirty();
|
||||||
|
|
||||||
@@ -948,6 +1270,10 @@ impl DavFileSystem for CarrierFs {
|
|||||||
);
|
);
|
||||||
inner.manifest.free_blocks.push(b);
|
inner.manifest.free_blocks.push(b);
|
||||||
}
|
}
|
||||||
|
if let Some(p_id) = dest.parent_id {
|
||||||
|
inner.index_remove_child(p_id, dest.id);
|
||||||
|
}
|
||||||
|
inner.children_index.remove(&dest.id);
|
||||||
inner.manifest.inodes.remove(&dest.id);
|
inner.manifest.inodes.remove(&dest.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,8 +1282,15 @@ impl DavFileSystem for CarrierFs {
|
|||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let old_parent_id = node.parent_id;
|
||||||
|
let new_parent_id = to_parent.id;
|
||||||
|
if let Some(old_p) = old_parent_id {
|
||||||
|
inner.index_remove_child(old_p, node.id);
|
||||||
|
}
|
||||||
|
inner.index_add_child(new_parent_id, node.id);
|
||||||
|
|
||||||
if let Some(inode) = inner.manifest.inodes.get_mut(&node.id) {
|
if let Some(inode) = inner.manifest.inodes.get_mut(&node.id) {
|
||||||
inode.parent_id = Some(to_parent.id);
|
inode.parent_id = Some(new_parent_id);
|
||||||
inode.name = to_name.to_string();
|
inode.name = to_name.to_string();
|
||||||
inode.modified_at = now;
|
inode.modified_at = now;
|
||||||
}
|
}
|
||||||
@@ -1013,6 +1346,10 @@ impl DavFileSystem for CarrierFs {
|
|||||||
);
|
);
|
||||||
inner.manifest.free_blocks.push(b);
|
inner.manifest.free_blocks.push(b);
|
||||||
}
|
}
|
||||||
|
if let Some(p_id) = dest.parent_id {
|
||||||
|
inner.index_remove_child(p_id, dest.id);
|
||||||
|
}
|
||||||
|
inner.children_index.remove(&dest.id);
|
||||||
inner.manifest.inodes.remove(&dest.id);
|
inner.manifest.inodes.remove(&dest.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,6 +1428,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_inode);
|
inner.manifest.inodes.insert(new_id, new_inode);
|
||||||
|
inner.index_add_child(to_parent.id, new_id);
|
||||||
inner.mark_dirty();
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+15
-8
@@ -423,17 +423,24 @@ pub async fn mount_container(
|
|||||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
||||||
}
|
}
|
||||||
if vault_id == 1 {
|
if vault_id == 1 {
|
||||||
if let Some((len, cap, pct)) = fs_clone.carrier_manifest_usage() {
|
if let Some(corrupted) = fs_clone.carrier_corrupted_pages() {
|
||||||
if len >= crate::carrier::CARRIER_MANIFEST_WARN_THRESHOLD {
|
if corrupted > 0 {
|
||||||
println!(
|
println!(
|
||||||
" {} Warnung (C-04): Hidden-Vault Manifest zu {:.1}% gefüllt ({}/{} Bytes).",
|
" {} Warnung (D-01): {} beschädigte Inode-Tabellenseite(n) im Hidden Vault übersprungen!",
|
||||||
ui::yellow("[!]"),
|
ui::red("[!]"),
|
||||||
pct,
|
corrupted
|
||||||
len,
|
|
||||||
cap
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((free_blocks, total_blocks)) = fs_clone.carrier_block_stats() {
|
||||||
|
if total_blocks > 0
|
||||||
|
&& (free_blocks < 20 || (free_blocks as f64 / total_blocks as f64) < 0.05)
|
||||||
|
{
|
||||||
println!(
|
println!(
|
||||||
" Empfohlene Obergrenze beachten: ca. 7.000 Dateien/Verzeichnisse im Hidden Vault."
|
" {} Warnung: Hidden Vault hat nur noch {} freie Blöcke (von {}).",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
free_blocks,
|
||||||
|
total_blocks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-12
@@ -713,8 +713,8 @@ impl Database {
|
|||||||
)?;
|
)?;
|
||||||
let c_id = conn.last_insert_rowid();
|
let c_id = conn.last_insert_rowid();
|
||||||
|
|
||||||
// Berechne Blockanzahl (min. 2 Blöcke: Block 0 für Manifest, Block 1+ für Nutzdaten)
|
// Berechne Blockanzahl (min. 3 Blöcke: Block 0/1 für Superblock, Block 2 für Inode-Seite)
|
||||||
let total_blocks = c_size.div_ceil(CHUNK_SIZE as u64).max(2) as u32;
|
let total_blocks = c_size.div_ceil(CHUNK_SIZE as u64).max(3) as u32;
|
||||||
|
|
||||||
// Slot 1 (Hidden Vault) einfügen
|
// Slot 1 (Hidden Vault) einfügen
|
||||||
let params_json_1 = serde_json::to_string(h_params)?;
|
let params_json_1 = serde_json::to_string(h_params)?;
|
||||||
@@ -732,13 +732,67 @@ impl Database {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Initialisiere CarrierManifest für redundante Blöcke 0 und 1 (C-02)
|
// Initialisiere Carrier-Format V2 (Superblock auf Block 0/1, Inode-Seite auf Block 2)
|
||||||
let manifest = crate::carrier::CarrierManifest::new(total_blocks);
|
let manifest = crate::carrier::CarrierManifest::new(total_blocks);
|
||||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
|
||||||
|
|
||||||
// 1. Block 0 schreiben (Primärkopie, Gen 0)
|
// 1. Block 2 schreiben: Erste Inode-Tabellenseite mit Wurzelverzeichnis '/'
|
||||||
|
let root_page = crate::carrier::CarrierInodePage {
|
||||||
|
magic: *crate::carrier::CARRIER_PAGE_MAGIC,
|
||||||
|
entries: manifest.inodes.values().cloned().collect(),
|
||||||
|
};
|
||||||
|
let root_page_bytes = serde_json::to_vec(&root_page)?;
|
||||||
|
|
||||||
|
let (inner_ct_2, inner_nonce_2, inner_tag_2) =
|
||||||
|
crate::crypto::encrypt_chunk(dek_1, c_id, 2, &root_page_bytes, FORMAT_VERSION, 0)?;
|
||||||
|
let inner_ct_len_2 = inner_ct_2.len() as u32;
|
||||||
|
|
||||||
|
let mut outer_plaintext_2 = vec![0u8; CHUNK_SIZE];
|
||||||
|
OsRng.fill_bytes(&mut outer_plaintext_2);
|
||||||
|
|
||||||
|
outer_plaintext_2[0..12].copy_from_slice(&inner_nonce_2);
|
||||||
|
outer_plaintext_2[12..28].copy_from_slice(&inner_tag_2);
|
||||||
|
outer_plaintext_2[28..32].copy_from_slice(&inner_ct_len_2.to_le_bytes());
|
||||||
|
let ct_end_2 = 32 + inner_ct_2.len();
|
||||||
|
if ct_end_2 > CHUNK_SIZE {
|
||||||
|
bail!("Inode-Page-Payload zu groß für Block 2");
|
||||||
|
}
|
||||||
|
outer_plaintext_2[32..ct_end_2].copy_from_slice(&inner_ct_2);
|
||||||
|
|
||||||
|
let (outer_ct_2, outer_nonce_2, outer_tag_2) = crate::crypto::encrypt_chunk(
|
||||||
|
dek_0,
|
||||||
|
c_id,
|
||||||
|
2,
|
||||||
|
&outer_plaintext_2,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
0,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
|
VALUES (?1, 2, 0, ?2, ?3, ?4)",
|
||||||
|
params![
|
||||||
|
c_id,
|
||||||
|
outer_nonce_2.as_slice(),
|
||||||
|
outer_tag_2.as_slice(),
|
||||||
|
outer_ct_2
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 2. Superblock (Block 0 & 1, redundante Zweitkopie C-02, Format V2)
|
||||||
|
let superblock = crate::carrier::CarrierSuperblock {
|
||||||
|
magic: *crate::carrier::CARRIER_MAGIC,
|
||||||
|
version: crate::carrier::CARRIER_VERSION_V2,
|
||||||
|
manifest_generation: 0,
|
||||||
|
total_blocks,
|
||||||
|
free_blocks: manifest.free_blocks.clone(),
|
||||||
|
next_inode_id: manifest.next_inode_id,
|
||||||
|
page_block_indices: vec![2],
|
||||||
|
};
|
||||||
|
let sb_bytes = serde_json::to_vec(&superblock)?;
|
||||||
|
|
||||||
|
// Block 0 schreiben (Primärkopie)
|
||||||
let (inner_ct_0, inner_nonce_0, inner_tag_0) =
|
let (inner_ct_0, inner_nonce_0, inner_tag_0) =
|
||||||
crate::crypto::encrypt_chunk(dek_1, c_id, 0, &manifest_bytes, FORMAT_VERSION, 0)?;
|
crate::crypto::encrypt_chunk(dek_1, c_id, 0, &sb_bytes, FORMAT_VERSION, 0)?;
|
||||||
let inner_ct_len_0 = inner_ct_0.len() as u32;
|
let inner_ct_len_0 = inner_ct_0.len() as u32;
|
||||||
|
|
||||||
let mut outer_plaintext_0 = vec![0u8; CHUNK_SIZE];
|
let mut outer_plaintext_0 = vec![0u8; CHUNK_SIZE];
|
||||||
@@ -749,7 +803,7 @@ impl Database {
|
|||||||
outer_plaintext_0[28..32].copy_from_slice(&inner_ct_len_0.to_le_bytes());
|
outer_plaintext_0[28..32].copy_from_slice(&inner_ct_len_0.to_le_bytes());
|
||||||
let ct_end_0 = 32 + inner_ct_0.len();
|
let ct_end_0 = 32 + inner_ct_0.len();
|
||||||
if ct_end_0 > CHUNK_SIZE {
|
if ct_end_0 > CHUNK_SIZE {
|
||||||
bail!("Manifest-Payload zu groß für Block 0");
|
bail!("Superblock-Payload zu groß für Block 0");
|
||||||
}
|
}
|
||||||
outer_plaintext_0[32..ct_end_0].copy_from_slice(&inner_ct_0);
|
outer_plaintext_0[32..ct_end_0].copy_from_slice(&inner_ct_0);
|
||||||
|
|
||||||
@@ -773,9 +827,9 @@ impl Database {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 2. Block 1 schreiben (Redundante Zweitkopie, Gen 0, C-02)
|
// Block 1 schreiben (Redundante Zweitkopie, C-02)
|
||||||
let (inner_ct_1, inner_nonce_1, inner_tag_1) =
|
let (inner_ct_1, inner_nonce_1, inner_tag_1) =
|
||||||
crate::crypto::encrypt_chunk(dek_1, c_id, 1, &manifest_bytes, FORMAT_VERSION, 0)?;
|
crate::crypto::encrypt_chunk(dek_1, c_id, 1, &sb_bytes, FORMAT_VERSION, 0)?;
|
||||||
let inner_ct_len_1 = inner_ct_1.len() as u32;
|
let inner_ct_len_1 = inner_ct_1.len() as u32;
|
||||||
|
|
||||||
let mut outer_plaintext_1 = vec![0u8; CHUNK_SIZE];
|
let mut outer_plaintext_1 = vec![0u8; CHUNK_SIZE];
|
||||||
@@ -786,7 +840,7 @@ impl Database {
|
|||||||
outer_plaintext_1[28..32].copy_from_slice(&inner_ct_len_1.to_le_bytes());
|
outer_plaintext_1[28..32].copy_from_slice(&inner_ct_len_1.to_le_bytes());
|
||||||
let ct_end_1 = 32 + inner_ct_1.len();
|
let ct_end_1 = 32 + inner_ct_1.len();
|
||||||
if ct_end_1 > CHUNK_SIZE {
|
if ct_end_1 > CHUNK_SIZE {
|
||||||
bail!("Manifest-Payload zu groß für Block 1");
|
bail!("Superblock-Payload zu groß für Block 1");
|
||||||
}
|
}
|
||||||
outer_plaintext_1[32..ct_end_1].copy_from_slice(&inner_ct_1);
|
outer_plaintext_1[32..ct_end_1].copy_from_slice(&inner_ct_1);
|
||||||
|
|
||||||
@@ -810,7 +864,7 @@ impl Database {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Blöcke 2..total_blocks-1 mit DEK_0 vorallokieren (C-02)
|
// 3. Blöcke 3..total_blocks-1 mit DEK_0 vorallokieren
|
||||||
let mut chunk_stmt = conn.prepare(
|
let mut chunk_stmt = conn.prepare(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
||||||
@@ -820,7 +874,7 @@ impl Database {
|
|||||||
OsRng.fill_bytes(&mut dummy_noise);
|
OsRng.fill_bytes(&mut dummy_noise);
|
||||||
|
|
||||||
conn.execute_batch("BEGIN TRANSACTION;")?;
|
conn.execute_batch("BEGIN TRANSACTION;")?;
|
||||||
for b in 2..total_blocks {
|
for b in 3..total_blocks {
|
||||||
let (ct, nonce, tag) =
|
let (ct, nonce, tag) =
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION, 0)?;
|
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION, 0)?;
|
||||||
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;
|
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;
|
||||||
|
|||||||
+12
@@ -799,6 +799,18 @@ impl SanctumFs {
|
|||||||
self.carrier_fs.as_ref().map(|cfs| cfs.manifest_usage())
|
self.carrier_fs.as_ref().map(|cfs| cfs.manifest_usage())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn carrier_corrupted_pages(&self) -> Option<usize> {
|
||||||
|
self.carrier_fs.as_ref().map(|cfs| cfs.corrupted_pages())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn carrier_block_stats(&self) -> Option<(usize, u32)> {
|
||||||
|
self.carrier_fs.as_ref().map(|cfs| cfs.block_stats())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn carrier_page_count(&self) -> Option<usize> {
|
||||||
|
self.carrier_fs.as_ref().map(|cfs| cfs.page_count())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn leak_count(&self) -> u64 {
|
pub fn leak_count(&self) -> u64 {
|
||||||
self.leak_counter().load(Ordering::Relaxed)
|
self.leak_counter().load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1436,21 +1436,28 @@ async fn test_c04_manifest_capacity_limit_and_warning() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync_manifest() MUSS jetzt mit Fehler abbrechen
|
// In Format V2 (v0.9.0): Die 1-MB-Grenze bricht NICHT mehr ab, sondern allokiert eine zusätzliche Seite
|
||||||
let err = cfs_loaded.sync_manifest().unwrap_err();
|
cfs_loaded
|
||||||
let err_str = err.to_string();
|
.sync_manifest()
|
||||||
|
.expect("sync_manifest with > 1 MB must succeed in V2 via paged manifest");
|
||||||
assert!(
|
assert!(
|
||||||
err_str.contains("Carrier-Manifest überschreitet die maximale Blockgröße"),
|
cfs_loaded.page_count() >= 2,
|
||||||
"Error message must specify carrier capacity limit: {}",
|
"Page count must expand to at least 2 pages"
|
||||||
err_str
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
err_str.contains("7.000 Dateien/Verzeichnisse"),
|
|
||||||
"Error message must recommend file limit: {}",
|
|
||||||
err_str
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reloaden und prüfen, ob beide Seiten geladen werden
|
||||||
drop(cfs_loaded);
|
drop(cfs_loaded);
|
||||||
|
let cfs_paged = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("CarrierFs load paged manifest");
|
||||||
|
assert!(cfs_paged.page_count() >= 2);
|
||||||
|
drop(cfs_paged);
|
||||||
drop(db);
|
drop(db);
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,686 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use dav_server::davpath::DavPath;
|
||||||
|
use dav_server::fs::{DavFileSystem, OpenOptions};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
|
||||||
|
use sanctum::carrier::{
|
||||||
|
write_carrier_block, CarrierFs, CarrierFsInner, CarrierInode, CarrierManifest, CARRIER_MAGIC,
|
||||||
|
CARRIER_VERSION, CARRIER_VERSION_V2,
|
||||||
|
};
|
||||||
|
use sanctum::crypto::{
|
||||||
|
derive_kek, generate_dek, generate_salt, wrap_slot0_payload, wrap_slot1_payload, KdfParams,
|
||||||
|
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||||
|
};
|
||||||
|
use sanctum::storage::{Database, UnlockedKeys};
|
||||||
|
|
||||||
|
fn temp_db_path(prefix: &str) -> PathBuf {
|
||||||
|
let mut path = std::env::temp_dir();
|
||||||
|
let id: u64 = OsRng.next_u64();
|
||||||
|
path.push(format!("sanctum_test_v2_{}_{}.sanctum", prefix, id));
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestEnv {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub db: Database,
|
||||||
|
pub carrier_node_id: i64,
|
||||||
|
pub auth_hidden: UnlockedKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestEnv {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_file(&self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_carrier_env(prefix: &str, size_mb: usize) -> TestEnv {
|
||||||
|
let path = temp_db_path(prefix);
|
||||||
|
let carrier_size_bytes = (size_mb * 1024 * 1024) as u64;
|
||||||
|
let carrier_name = "carrier_v2.dat";
|
||||||
|
let pass_decoy = "DecoyPassword2026!";
|
||||||
|
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||||
|
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let salt_0 = generate_salt();
|
||||||
|
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||||
|
let dek_0 = generate_dek();
|
||||||
|
|
||||||
|
let salt_1 = generate_salt();
|
||||||
|
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||||
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
|
let carrier_node_id = 3i64;
|
||||||
|
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
|
|
||||||
|
let db = Database::open(&path).expect("Open database");
|
||||||
|
db.init_schema_with_carrier(
|
||||||
|
&salt_0,
|
||||||
|
&kdf_params,
|
||||||
|
&wrapped_0,
|
||||||
|
&nonce_0,
|
||||||
|
&tag_0,
|
||||||
|
Some((
|
||||||
|
carrier_name,
|
||||||
|
carrier_size_bytes,
|
||||||
|
&salt_1,
|
||||||
|
&kdf_params,
|
||||||
|
&wrapped_1,
|
||||||
|
&nonce_1,
|
||||||
|
&tag_1,
|
||||||
|
&dek_0,
|
||||||
|
&dek_1,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.expect("Init carrier schema");
|
||||||
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
|
let meta = db.read_meta().unwrap();
|
||||||
|
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||||
|
|
||||||
|
TestEnv {
|
||||||
|
path,
|
||||||
|
db,
|
||||||
|
carrier_node_id,
|
||||||
|
auth_hidden,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_carrier_fs(env: &TestEnv) -> CarrierFs {
|
||||||
|
CarrierFs::load(
|
||||||
|
env.db.clone(),
|
||||||
|
env.carrier_node_id,
|
||||||
|
std::sync::Arc::new(env.auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(env.auth_hidden.dek().clone()),
|
||||||
|
env.auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("Load CarrierFs")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 1. Kapazitätstest: >10.000 Inodes künstlich erzeugen, über mehrere Seiten speichern und neu laden
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_capacity_large_inode_table() {
|
||||||
|
let env = setup_carrier_env("capacity_large", 25);
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
|
||||||
|
// Initial hat ein frischer Container 1 Seite (Block 2)
|
||||||
|
assert_eq!(cfs.page_count(), 1);
|
||||||
|
|
||||||
|
// 10.000 Inodes direkt in den Speicher einfügen
|
||||||
|
{
|
||||||
|
let mut inner = cfs.inner.lock().unwrap();
|
||||||
|
for i in 0..10_000 {
|
||||||
|
let id = inner.manifest.next_inode_id;
|
||||||
|
inner.manifest.next_inode_id += 1;
|
||||||
|
let inode = CarrierInode {
|
||||||
|
id,
|
||||||
|
parent_id: Some(1),
|
||||||
|
name: format!("file_{:05}.txt", i),
|
||||||
|
is_dir: false,
|
||||||
|
size: i % 500,
|
||||||
|
created_at: 1_700_000_000 + i,
|
||||||
|
modified_at: 1_700_000_000 + i,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
};
|
||||||
|
inner.manifest.inodes.insert(id, inode);
|
||||||
|
inner.index_add_child(1, id);
|
||||||
|
}
|
||||||
|
inner.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichern erzwingen
|
||||||
|
cfs.sync_manifest().expect("Sync 10.000 inodes");
|
||||||
|
|
||||||
|
// In V2 müssen die 10.000 Inodes auf mindestens 2 Seiten aufgeteilt worden sein
|
||||||
|
let pages_after_save = cfs.page_count();
|
||||||
|
assert!(
|
||||||
|
pages_after_save >= 2,
|
||||||
|
"10.000 Inodes müssen auf mindestens 2 Seiten aufgeteilt werden, tatsächlich: {}",
|
||||||
|
pages_after_save
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// Neu aus Trägerdatei laden
|
||||||
|
let cfs_reloaded = load_carrier_fs(&env);
|
||||||
|
assert_eq!(cfs_reloaded.page_count(), pages_after_save);
|
||||||
|
assert_eq!(cfs_reloaded.corrupted_pages(), 0);
|
||||||
|
|
||||||
|
// Prüfe, dass beliebige Dateien korrekt existieren und Pfade auflösen
|
||||||
|
let test_file_1 = cfs_reloaded.resolve_path("/file_00000.txt");
|
||||||
|
assert!(test_file_1.is_some());
|
||||||
|
assert_eq!(test_file_1.unwrap().name, "file_00000.txt");
|
||||||
|
|
||||||
|
let test_file_mid = cfs_reloaded.resolve_path("/file_04999.txt");
|
||||||
|
assert!(test_file_mid.is_some());
|
||||||
|
assert_eq!(test_file_mid.unwrap().name, "file_04999.txt");
|
||||||
|
|
||||||
|
let test_file_end = cfs_reloaded.resolve_path("/file_09999.txt");
|
||||||
|
assert!(test_file_end.is_some());
|
||||||
|
assert_eq!(test_file_end.unwrap().name, "file_09999.txt");
|
||||||
|
|
||||||
|
let nonexistent = cfs_reloaded.resolve_path("/file_10000.txt");
|
||||||
|
assert!(nonexistent.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2. Migrationstest: Einen V1-Container laden, modifizieren, als V2 speichern und verifizieren
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_v1_to_v2_migration() {
|
||||||
|
let env = setup_carrier_env("migration_v1_v2", 10);
|
||||||
|
|
||||||
|
// Einen V1-Container konstruieren: Block 0 und Block 1 halten CarrierManifest mit version = 1
|
||||||
|
let mut v1_manifest = CarrierManifest {
|
||||||
|
magic: *CARRIER_MAGIC,
|
||||||
|
version: CARRIER_VERSION, // 1
|
||||||
|
manifest_generation: 1,
|
||||||
|
total_blocks: 10,
|
||||||
|
free_blocks: (2..10).collect(),
|
||||||
|
next_inode_id: 3,
|
||||||
|
page_block_indices: Vec::new(),
|
||||||
|
inodes: HashMap::new(),
|
||||||
|
};
|
||||||
|
v1_manifest.inodes.insert(
|
||||||
|
1,
|
||||||
|
CarrierInode {
|
||||||
|
id: 1,
|
||||||
|
parent_id: None,
|
||||||
|
name: String::new(),
|
||||||
|
is_dir: true,
|
||||||
|
size: 0,
|
||||||
|
created_at: 1000,
|
||||||
|
modified_at: 1000,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
v1_manifest.inodes.insert(
|
||||||
|
2,
|
||||||
|
CarrierInode {
|
||||||
|
id: 2,
|
||||||
|
parent_id: Some(1),
|
||||||
|
name: "original_v1.txt".to_string(),
|
||||||
|
is_dir: false,
|
||||||
|
size: 128,
|
||||||
|
created_at: 1000,
|
||||||
|
modified_at: 1000,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let v1_bytes = serde_json::to_vec(&v1_manifest).unwrap();
|
||||||
|
let dek_outer = env.auth_hidden.carrier_dek().unwrap();
|
||||||
|
let dek_inner = env.auth_hidden.dek();
|
||||||
|
write_carrier_block(
|
||||||
|
&env.db,
|
||||||
|
env.carrier_node_id,
|
||||||
|
0,
|
||||||
|
&dek_outer,
|
||||||
|
&dek_inner,
|
||||||
|
&v1_bytes,
|
||||||
|
env.auth_hidden.version(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
write_carrier_block(
|
||||||
|
&env.db,
|
||||||
|
env.carrier_node_id,
|
||||||
|
1,
|
||||||
|
&dek_outer,
|
||||||
|
&dek_inner,
|
||||||
|
&v1_bytes,
|
||||||
|
env.auth_hidden.version(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 1. Unter v0.9.0 laden: V1-Format wird transparent erkannt
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
{
|
||||||
|
let inner = cfs.inner.lock().unwrap();
|
||||||
|
assert_eq!(inner.manifest.version, CARRIER_VERSION);
|
||||||
|
}
|
||||||
|
let orig = cfs.resolve_path("/original_v1.txt");
|
||||||
|
assert!(orig.is_some());
|
||||||
|
assert_eq!(orig.unwrap().id, 2);
|
||||||
|
|
||||||
|
// 2. Änderung vornehmen: Datei hinzufügen (löst Dirty-Tracking aus)
|
||||||
|
let new_file_path = DavPath::new("/new_v2.txt").unwrap();
|
||||||
|
let mut file = cfs
|
||||||
|
.open(
|
||||||
|
&new_file_path,
|
||||||
|
OpenOptions {
|
||||||
|
create_new: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Create new file in migrated container");
|
||||||
|
file.write_bytes(Bytes::from_static(b"migrated to v2"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
file.flush().await.unwrap();
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// Explizit synchronisieren
|
||||||
|
cfs.sync_manifest().expect("Sync manifest during migration");
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// 3. Neu laden: Jetzt muss das Dateisystem im Format V2 vorliegen
|
||||||
|
let cfs_reloaded = load_carrier_fs(&env);
|
||||||
|
{
|
||||||
|
let inner = cfs_reloaded.inner.lock().unwrap();
|
||||||
|
assert_eq!(inner.manifest.version, CARRIER_VERSION_V2);
|
||||||
|
assert!(!inner.manifest.page_block_indices.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beide Dateien müssen vorhanden sein
|
||||||
|
assert!(cfs_reloaded.resolve_path("/original_v1.txt").is_some());
|
||||||
|
assert!(cfs_reloaded.resolve_path("/new_v2.txt").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3. Teilausfalltest (D-01 Fail-Soft): Beschädigung einer einzelnen Inode-Seite führt nicht zum Totalverlust
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_partial_page_corruption_resilience() {
|
||||||
|
let env = setup_carrier_env("partial_corruption", 20);
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
|
||||||
|
// Genügend Inodes erzeugen, um 2 Seiten zu belegen
|
||||||
|
{
|
||||||
|
let mut inner = cfs.inner.lock().unwrap();
|
||||||
|
for i in 0..1500 {
|
||||||
|
let id = inner.manifest.next_inode_id;
|
||||||
|
inner.manifest.next_inode_id += 1;
|
||||||
|
let name = if i < 750 {
|
||||||
|
format!("alpha_{:04}_{}", i, "a".repeat(800))
|
||||||
|
} else {
|
||||||
|
format!("omega_{:04}_{}", i, "z".repeat(800))
|
||||||
|
};
|
||||||
|
let inode = CarrierInode {
|
||||||
|
id,
|
||||||
|
parent_id: Some(1),
|
||||||
|
name,
|
||||||
|
is_dir: false,
|
||||||
|
size: 10,
|
||||||
|
created_at: 2000,
|
||||||
|
modified_at: 2000,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
};
|
||||||
|
inner.manifest.inodes.insert(id, inode);
|
||||||
|
inner.index_add_child(1, id);
|
||||||
|
}
|
||||||
|
inner.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
cfs.sync_manifest().expect("Sync 2 pages");
|
||||||
|
let page_blocks = {
|
||||||
|
let inner = cfs.inner.lock().unwrap();
|
||||||
|
inner.manifest.page_block_indices.clone()
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
page_blocks.len() >= 2,
|
||||||
|
"Benötigt mindestens 2 Seiten, hat: {}",
|
||||||
|
page_blocks.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let second_page_block = page_blocks[1];
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// 2. Gezielte Beschädigung: Zweite Seite in SQLite mit Rauschen überschreiben
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&env.path).unwrap();
|
||||||
|
let mut noise = vec![0u8; 500];
|
||||||
|
OsRng.fill_bytes(&mut noise);
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = ?3",
|
||||||
|
rusqlite::params![noise, env.carrier_node_id, second_page_block],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Neu laden: Darf NICHT abbrechen (Fail-Soft), sondern überspringt die defekte Seite
|
||||||
|
let cfs_reloaded = load_carrier_fs(&env);
|
||||||
|
assert_eq!(
|
||||||
|
cfs_reloaded.corrupted_pages(),
|
||||||
|
1,
|
||||||
|
"Muss genau 1 beschädigte Seite protokollieren"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wurzelverzeichnis '/' muss intakt sein
|
||||||
|
assert!(cfs_reloaded.resolve_path("/").is_some());
|
||||||
|
|
||||||
|
// Einträge der ersten, unbeschädigten Seite müssen weiterhin vollständig auffindbar sein
|
||||||
|
let alpha_0 = cfs_reloaded.resolve_path(&format!("/alpha_0000_{}", "a".repeat(800)));
|
||||||
|
assert!(
|
||||||
|
alpha_0.is_some(),
|
||||||
|
"Dateien aus der intakten Seite 0 müssen lesbar bleiben"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 4. Index-Konsistenztest (D-02): Operationsfolge gegen frisch aufgebauten Referenzindex abgleichen
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_children_index_consistency() {
|
||||||
|
let env = setup_carrier_env("children_consistency", 10);
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
|
||||||
|
// 1. Verzeichnisse anlegen
|
||||||
|
cfs.create_dir(&DavPath::new("/docs").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cfs.create_dir(&DavPath::new("/docs/work").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cfs.create_dir(&DavPath::new("/photos").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 2. Dateien anlegen
|
||||||
|
let opt = OpenOptions {
|
||||||
|
create_new: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut f1 = cfs
|
||||||
|
.open(&DavPath::new("/docs/report.txt").unwrap(), opt.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
f1.write_bytes(Bytes::from_static(b"report")).await.unwrap();
|
||||||
|
f1.flush().await.unwrap();
|
||||||
|
drop(f1);
|
||||||
|
|
||||||
|
let mut f2 = cfs
|
||||||
|
.open(&DavPath::new("/docs/work/notes.txt").unwrap(), opt.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
f2.write_bytes(Bytes::from_static(b"notes")).await.unwrap();
|
||||||
|
f2.flush().await.unwrap();
|
||||||
|
drop(f2);
|
||||||
|
|
||||||
|
let mut f3 = cfs
|
||||||
|
.open(&DavPath::new("/photos/pic.jpg").unwrap(), opt.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
f3.write_bytes(Bytes::from_static(b"pic")).await.unwrap();
|
||||||
|
f3.flush().await.unwrap();
|
||||||
|
drop(f3);
|
||||||
|
|
||||||
|
// 3. Verschieben/Umbenennen
|
||||||
|
cfs.rename(
|
||||||
|
&DavPath::new("/docs/work/notes.txt").unwrap(),
|
||||||
|
&DavPath::new("/photos/notes_moved.txt").unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 4. Datei löschen
|
||||||
|
cfs.remove_file(&DavPath::new("/docs/report.txt").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 5. Verzeichnis löschen
|
||||||
|
cfs.remove_dir(&DavPath::new("/docs/work").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 6. Abgleich: children_index gegen frisch aufgebauten Referenzindex prüfen
|
||||||
|
{
|
||||||
|
let inner = cfs.inner.lock().unwrap();
|
||||||
|
let reference_index = CarrierFsInner::build_children_index(&inner.manifest.inodes);
|
||||||
|
|
||||||
|
for (parent_id, ref_children) in &reference_index {
|
||||||
|
let actual_children = inner.children_index.get(parent_id);
|
||||||
|
assert!(
|
||||||
|
actual_children.is_some(),
|
||||||
|
"Parent {} fehlt im children_index",
|
||||||
|
parent_id
|
||||||
|
);
|
||||||
|
let mut ref_sorted = ref_children.clone();
|
||||||
|
ref_sorted.sort();
|
||||||
|
let mut act_sorted = actual_children.unwrap().clone();
|
||||||
|
act_sorted.sort();
|
||||||
|
assert_eq!(
|
||||||
|
ref_sorted, act_sorted,
|
||||||
|
"Inkonsistenz der Kinder für Parent {}",
|
||||||
|
parent_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auch die Umkehrung prüfen (keine Geister-Einträge)
|
||||||
|
for (parent_id, actual_children) in &inner.children_index {
|
||||||
|
if actual_children.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
reference_index.contains_key(parent_id),
|
||||||
|
"children_index enthält verwaisten Parent {}",
|
||||||
|
parent_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5. Performance-Sanity-Check: Pfadauflösung bei 5.000 Geschwistern muss sub-millisekündlich sein (D-02)
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_secondary_index_path_resolution_speed() {
|
||||||
|
let env = setup_carrier_env("perf_resolution", 15);
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
|
||||||
|
cfs.create_dir(&DavPath::new("/big_dir").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let big_dir_node = cfs.resolve_path("/big_dir").expect("big_dir node");
|
||||||
|
|
||||||
|
// 5.000 Kindknoten in /big_dir anlegen
|
||||||
|
{
|
||||||
|
let mut inner = cfs.inner.lock().unwrap();
|
||||||
|
for i in 0..5_000 {
|
||||||
|
let id = inner.manifest.next_inode_id;
|
||||||
|
inner.manifest.next_inode_id += 1;
|
||||||
|
let inode = CarrierInode {
|
||||||
|
id,
|
||||||
|
parent_id: Some(big_dir_node.id),
|
||||||
|
name: format!("child_{:04}.txt", i),
|
||||||
|
is_dir: false,
|
||||||
|
size: 10,
|
||||||
|
created_at: 3000,
|
||||||
|
modified_at: 3000,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
};
|
||||||
|
inner.manifest.inodes.insert(id, inode);
|
||||||
|
inner.index_add_child(big_dir_node.id, id);
|
||||||
|
}
|
||||||
|
inner.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messung: Pfadauflösung für den letzten Eintrag
|
||||||
|
let start = Instant::now();
|
||||||
|
let target = cfs.resolve_path("/big_dir/child_4999.txt");
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
|
assert!(target.is_some());
|
||||||
|
assert_eq!(target.unwrap().name, "child_4999.txt");
|
||||||
|
|
||||||
|
// Der Sekundärindex muss den Eintrag in unter 5 Millisekunden finden (typisch < 0.2 ms)
|
||||||
|
assert!(
|
||||||
|
elapsed.as_millis() < 5,
|
||||||
|
"Pfadauflösung dauerte zu lange: {:?}",
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 6. Platzmangel-Migrationstest: Migration eines Containers ohne freie Blöcke bricht sauber ab (D-03)
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_out_of_space_migration_aborts_cleanly() {
|
||||||
|
let env = setup_carrier_env("migration_no_space", 5);
|
||||||
|
|
||||||
|
// V1-Manifest mit 0 freien Blöcken (free_blocks ist leer)
|
||||||
|
let mut v1_manifest = CarrierManifest {
|
||||||
|
magic: *CARRIER_MAGIC,
|
||||||
|
version: CARRIER_VERSION,
|
||||||
|
manifest_generation: 1,
|
||||||
|
total_blocks: 2,
|
||||||
|
free_blocks: Vec::new(), // Keine freien Blöcke!
|
||||||
|
next_inode_id: 2,
|
||||||
|
page_block_indices: Vec::new(),
|
||||||
|
inodes: HashMap::new(),
|
||||||
|
};
|
||||||
|
v1_manifest.inodes.insert(
|
||||||
|
1,
|
||||||
|
CarrierInode {
|
||||||
|
id: 1,
|
||||||
|
parent_id: None,
|
||||||
|
name: String::new(),
|
||||||
|
is_dir: true,
|
||||||
|
size: 0,
|
||||||
|
created_at: 1000,
|
||||||
|
modified_at: 1000,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let v1_bytes = serde_json::to_vec(&v1_manifest).unwrap();
|
||||||
|
let dek_outer = env.auth_hidden.carrier_dek().unwrap();
|
||||||
|
let dek_inner = env.auth_hidden.dek();
|
||||||
|
write_carrier_block(
|
||||||
|
&env.db,
|
||||||
|
env.carrier_node_id,
|
||||||
|
0,
|
||||||
|
&dek_outer,
|
||||||
|
&dek_inner,
|
||||||
|
&v1_bytes,
|
||||||
|
env.auth_hidden.version(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
write_carrier_block(
|
||||||
|
&env.db,
|
||||||
|
env.carrier_node_id,
|
||||||
|
1,
|
||||||
|
&dek_outer,
|
||||||
|
&dek_inner,
|
||||||
|
&v1_bytes,
|
||||||
|
env.auth_hidden.version(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
|
||||||
|
// Versuch zu speichern muss mit klarer Fehlermeldung fehlschlagen
|
||||||
|
let res = {
|
||||||
|
let mut inner = cfs.inner.lock().unwrap();
|
||||||
|
inner.mark_dirty();
|
||||||
|
inner.save_manifest()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(res.is_err());
|
||||||
|
let err_msg = res.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Nicht genügend freie Blöcke"),
|
||||||
|
"Fehlermeldung muss auf Speichermangel hinweisen: {}",
|
||||||
|
err_msg
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// Trägerdatei muss weiterhin intakt als V1 ladbar sein
|
||||||
|
let cfs_recheck = load_carrier_fs(&env);
|
||||||
|
{
|
||||||
|
let inner = cfs_recheck.inner.lock().unwrap();
|
||||||
|
assert_eq!(inner.manifest.version, CARRIER_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 7. Seitenfreigabe (D-05): Überzählige Seitenblöcke werden bei Schrumpfung geschreddert und freigegeben
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_page_block_reclaiming_on_shrink() {
|
||||||
|
let env = setup_carrier_env("page_reclaiming", 25);
|
||||||
|
let cfs = load_carrier_fs(&env);
|
||||||
|
|
||||||
|
let initial_free = {
|
||||||
|
let inner = cfs.inner.lock().unwrap();
|
||||||
|
inner.manifest.free_blocks.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Viele Inodes erzeugen, sodass mindestens 3 Seiten belegt werden
|
||||||
|
let mut added_ids = Vec::new();
|
||||||
|
{
|
||||||
|
let mut inner = cfs.inner.lock().unwrap();
|
||||||
|
for i in 0..2500 {
|
||||||
|
let id = inner.manifest.next_inode_id;
|
||||||
|
inner.manifest.next_inode_id += 1;
|
||||||
|
added_ids.push(id);
|
||||||
|
let inode = CarrierInode {
|
||||||
|
id,
|
||||||
|
parent_id: Some(1),
|
||||||
|
name: format!("large_item_{:04}_{}", i, "x".repeat(800)),
|
||||||
|
is_dir: false,
|
||||||
|
size: 10,
|
||||||
|
created_at: 4000,
|
||||||
|
modified_at: 4000,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
};
|
||||||
|
inner.manifest.inodes.insert(id, inode);
|
||||||
|
inner.index_add_child(1, id);
|
||||||
|
}
|
||||||
|
inner.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
cfs.sync_manifest().expect("Sync 3 pages");
|
||||||
|
let pages_before = cfs.page_count();
|
||||||
|
assert!(
|
||||||
|
pages_before >= 3,
|
||||||
|
"Muss mindestens 3 Seiten belegen, hat: {}",
|
||||||
|
pages_before
|
||||||
|
);
|
||||||
|
|
||||||
|
let free_after_expand = {
|
||||||
|
let inner = cfs.inner.lock().unwrap();
|
||||||
|
inner.manifest.free_blocks.len()
|
||||||
|
};
|
||||||
|
assert_eq!(free_after_expand, initial_free - (pages_before - 1));
|
||||||
|
|
||||||
|
// 2. Fast alle Inodes entfernen, sodass nur noch 1 Seite benötigt wird
|
||||||
|
{
|
||||||
|
let mut inner = cfs.inner.lock().unwrap();
|
||||||
|
for id in added_ids {
|
||||||
|
inner.manifest.inodes.remove(&id);
|
||||||
|
inner.index_remove_child(1, id);
|
||||||
|
}
|
||||||
|
inner.mark_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
cfs.sync_manifest().expect("Sync after shrink");
|
||||||
|
|
||||||
|
// 3. Verifizieren: page_count ist wieder 1, freie Blöcke wurden zurückgegeben
|
||||||
|
let pages_after = cfs.page_count();
|
||||||
|
assert_eq!(
|
||||||
|
pages_after, 1,
|
||||||
|
"Nach Schrumpfung darf nur noch 1 Seite belegt sein"
|
||||||
|
);
|
||||||
|
|
||||||
|
let free_after_shrink = {
|
||||||
|
let inner = cfs.inner.lock().unwrap();
|
||||||
|
inner.manifest.free_blocks.len()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
free_after_shrink, initial_free,
|
||||||
|
"Alle überzähligen Seitenblöcke müssen an free_blocks zurückgegeben worden sein"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// Trägerdatei neu laden und Integrität prüfen
|
||||||
|
let cfs_reloaded = load_carrier_fs(&env);
|
||||||
|
assert_eq!(cfs_reloaded.page_count(), 1);
|
||||||
|
assert_eq!(cfs_reloaded.corrupted_pages(), 0);
|
||||||
|
assert!(cfs_reloaded.resolve_path("/").is_some());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user