feat(carrier): implement carrier format V2 paged manifest and secondary index
This commit is contained in:
+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_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_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)]
|
||||
pub struct CarrierManifest {
|
||||
pub magic: [u8; 8],
|
||||
@@ -40,6 +70,9 @@ pub struct CarrierManifest {
|
||||
pub total_blocks: u32,
|
||||
pub free_blocks: Vec<u32>,
|
||||
pub next_inode_id: i64,
|
||||
#[serde(default)]
|
||||
pub page_block_indices: Vec<u32>,
|
||||
#[serde(default)]
|
||||
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.
|
||||
let free_blocks = (2..total_blocks).collect();
|
||||
// Block 0 und Block 1 sind für den Superblock reserviert (C-02).
|
||||
// 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 {
|
||||
magic: *CARRIER_MAGIC,
|
||||
version: CARRIER_VERSION,
|
||||
version: CARRIER_VERSION_V2,
|
||||
manifest_generation: 0,
|
||||
total_blocks,
|
||||
free_blocks,
|
||||
next_inode_id: 2,
|
||||
page_block_indices,
|
||||
inodes,
|
||||
}
|
||||
}
|
||||
@@ -279,6 +324,8 @@ pub struct CarrierFsInner {
|
||||
pub custom_leak_rules: Arc<Vec<String>>,
|
||||
pub leak_counter: Arc<AtomicU64>,
|
||||
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_save_count: u64,
|
||||
pub dirty_ops_count: u32,
|
||||
@@ -291,13 +338,31 @@ impl CarrierFsInner {
|
||||
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).
|
||||
/// 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) {
|
||||
self.manifest_dirty = true;
|
||||
self.dirty_ops_count += 1;
|
||||
@@ -314,15 +379,37 @@ impl CarrierFsInner {
|
||||
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) {
|
||||
let len = serde_json::to_vec(&self.manifest)
|
||||
.map(|v| v.len())
|
||||
.unwrap_or(0);
|
||||
let pct = (len as f64 / CARRIER_MANIFEST_CAPACITY as f64) * 100.0;
|
||||
(len, CARRIER_MANIFEST_CAPACITY, pct)
|
||||
let pages_count = self.manifest.page_block_indices.len().max(1);
|
||||
let total_page_capacity = pages_count * CARRIER_MANIFEST_CAPACITY;
|
||||
let mut total_bytes = 0usize;
|
||||
for inode in self.manifest.inodes.values() {
|
||||
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<()> {
|
||||
self.manifest_save_count += 1;
|
||||
self.manifest_dirty = false;
|
||||
@@ -332,26 +419,119 @@ impl CarrierFsInner {
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
self.manifest.manifest_generation += 1;
|
||||
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
||||
if manifest_bytes.len() > CARRIER_MANIFEST_CAPACITY {
|
||||
bail!(
|
||||
"Carrier-Manifest überschreitet die maximale Blockgröße ({} > {} Bytes). Empfohlene Obergrenze ca. 7.000 Dateien/Verzeichnisse beachten.",
|
||||
manifest_bytes.len(),
|
||||
CARRIER_MANIFEST_CAPACITY
|
||||
);
|
||||
|
||||
// D-04: Inodes auf Seiten aufteilen (deterministisch nach Inode-ID sortiert)
|
||||
let mut sorted_inodes: Vec<CarrierInode> = self.manifest.inodes.values().cloned().collect();
|
||||
sorted_inodes.sort_by_key(|i| i.id);
|
||||
|
||||
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 {
|
||||
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(),
|
||||
if !current_entries.is_empty() || pages.is_empty() {
|
||||
pages.push(CarrierInodePage {
|
||||
magic: *CARRIER_PAGE_MAGIC,
|
||||
entries: current_entries,
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
0
|
||||
} else {
|
||||
@@ -364,9 +544,20 @@ impl CarrierFsInner {
|
||||
target_block,
|
||||
&self.dek_outer,
|
||||
&self.dek_inner,
|
||||
&manifest_bytes,
|
||||
&sb_bytes,
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -389,6 +580,7 @@ impl CarrierFsInner {
|
||||
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> {
|
||||
let trimmed = raw_path.trim_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
@@ -399,10 +591,17 @@ impl CarrierFsInner {
|
||||
let mut current_id = 1i64;
|
||||
|
||||
for (idx, segment) in segments.iter().enumerate() {
|
||||
let child = self.manifest.inodes.values().find(|inode| {
|
||||
inode.parent_id == Some(current_id) && inode.name.as_str() == *segment
|
||||
let child_ids = self.children_index.get(¤t_id)?;
|
||||
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 {
|
||||
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).
|
||||
#[derive(Clone)]
|
||||
pub struct CarrierFs {
|
||||
inner: Arc<Mutex<CarrierFsInner>>,
|
||||
pub inner: Arc<Mutex<CarrierFsInner>>,
|
||||
}
|
||||
|
||||
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).
|
||||
pub fn load_with_leak_rules(
|
||||
db: Database,
|
||||
@@ -454,68 +668,62 @@ impl CarrierFs {
|
||||
anti_leak: bool,
|
||||
custom_leak_rules: Vec<String>,
|
||||
) -> Result<Self> {
|
||||
// C-02: Versuche beide redundante Blöcke (Block 0 und Block 1) zu lesen und zu verifizieren
|
||||
let block_0_res = read_carrier_block(
|
||||
&db,
|
||||
carrier_node_id,
|
||||
0,
|
||||
&dek_outer,
|
||||
&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 0");
|
||||
let read_and_parse_block = |block_idx: u32| -> Result<LoadedBlock> {
|
||||
let bytes = read_carrier_block(
|
||||
&db,
|
||||
carrier_node_id,
|
||||
block_idx,
|
||||
&dek_outer,
|
||||
&dek_inner,
|
||||
format_version,
|
||||
)?;
|
||||
let probe: VersionProbe = serde_json::from_slice(&bytes)?;
|
||||
if probe.magic != *CARRIER_MAGIC {
|
||||
bail!("Ungültige Carrier-Magic-Bytes in Block {block_idx}");
|
||||
}
|
||||
Ok(m)
|
||||
});
|
||||
|
||||
let block_1_res = read_carrier_block(
|
||||
&db,
|
||||
carrier_node_id,
|
||||
1,
|
||||
&dek_outer,
|
||||
&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");
|
||||
if probe.version == CARRIER_VERSION {
|
||||
let m: CarrierManifest = serde_json::from_slice(&bytes)?;
|
||||
Ok(LoadedBlock::V1(m))
|
||||
} else if probe.version == CARRIER_VERSION_V2 {
|
||||
let sb: CarrierSuperblock = serde_json::from_slice(&bytes)?;
|
||||
Ok(LoadedBlock::V2(sb))
|
||||
} else {
|
||||
bail!("Unbekannte Carrier-Formatversion: {}", probe.version);
|
||||
}
|
||||
Ok(m)
|
||||
});
|
||||
};
|
||||
|
||||
let mut manifest = match (block_0_res, block_1_res) {
|
||||
(Ok(m0), Ok(m1)) => {
|
||||
if m1.manifest_generation > m0.manifest_generation {
|
||||
let block_0_res = read_and_parse_block(0);
|
||||
let block_1_res = read_and_parse_block(1);
|
||||
|
||||
let loaded_block = match (block_0_res, block_1_res) {
|
||||
(Ok(b0), Ok(b1)) => {
|
||||
if b1.generation() > b0.generation() {
|
||||
debug!(
|
||||
"C-02: Beide Manifest-Kopien intakt. Wähle neuere Generation {} aus Block 1 (Block 0: Gen {})",
|
||||
m1.manifest_generation, m0.manifest_generation
|
||||
"C-02: Beide Superblock-Kopien intakt. Wähle neuere Generation {} aus Block 1 (Block 0: Gen {})",
|
||||
b1.generation(), b0.generation()
|
||||
);
|
||||
m1
|
||||
b1
|
||||
} else {
|
||||
debug!(
|
||||
"C-02: Beide Manifest-Kopien intakt. Wähle Generation {} aus Block 0 (Block 1: Gen {})",
|
||||
m0.manifest_generation, m1.manifest_generation
|
||||
"C-02: Beide Superblock-Kopien intakt. Wähle Generation {} aus Block 0 (Block 1: Gen {})",
|
||||
b0.generation(), b1.generation()
|
||||
);
|
||||
m0
|
||||
b0
|
||||
}
|
||||
}
|
||||
(Ok(m0), Err(e1)) => {
|
||||
(Ok(b0), Err(e1)) => {
|
||||
debug!(
|
||||
"C-02: Verwende primäres Manifest aus Block 0 (Gen {}). Block 1 unlesbar oder uninitialisiert: {}",
|
||||
m0.manifest_generation, e1
|
||||
"C-02: Verwende primären Superblock aus Block 0 (Gen {}). Block 1 unlesbar oder uninitialisiert: {}",
|
||||
b0.generation(), e1
|
||||
);
|
||||
m0
|
||||
b0
|
||||
}
|
||||
(Err(e0), Ok(m1)) => {
|
||||
(Err(e0), Ok(b1)) => {
|
||||
warn!(
|
||||
"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)) => {
|
||||
bail!(
|
||||
@@ -525,20 +733,101 @@ impl CarrierFs {
|
||||
}
|
||||
};
|
||||
|
||||
// Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Manifest-Kopie reservieren
|
||||
manifest.free_blocks.retain(|&b| b >= 2);
|
||||
|
||||
// C-04: Warnung bei Füllstand über 80% beim Mounten
|
||||
if let Ok(manifest_bytes) = serde_json::to_vec(&manifest) {
|
||||
if manifest_bytes.len() >= CARRIER_MANIFEST_WARN_THRESHOLD {
|
||||
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
|
||||
);
|
||||
let mut corrupted_pages = 0usize;
|
||||
let manifest = match loaded_block {
|
||||
LoadedBlock::V1(mut m) => {
|
||||
// Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Kopie reservieren
|
||||
m.free_blocks.retain(|&b| b >= 2);
|
||||
m
|
||||
}
|
||||
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()
|
||||
@@ -560,6 +849,8 @@ impl CarrierFs {
|
||||
custom_leak_rules: Arc::new(custom_leak_rules),
|
||||
leak_counter: Arc::new(AtomicU64::new(0)),
|
||||
manifest,
|
||||
children_index,
|
||||
corrupted_pages,
|
||||
manifest_dirty: false,
|
||||
manifest_save_count: 0,
|
||||
dirty_ops_count: 0,
|
||||
@@ -612,6 +903,26 @@ impl CarrierFs {
|
||||
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> {
|
||||
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.leak_counter.clone()
|
||||
@@ -721,6 +1032,7 @@ impl DavFileSystem for CarrierFs {
|
||||
};
|
||||
|
||||
inner.manifest.inodes.insert(new_id, new_inode.clone());
|
||||
inner.index_add_child(parent.id, new_id);
|
||||
inner.mark_dirty();
|
||||
new_inode
|
||||
} else {
|
||||
@@ -750,11 +1062,14 @@ impl DavFileSystem for CarrierFs {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = inner
|
||||
.manifest
|
||||
.inodes
|
||||
.values()
|
||||
.filter(|child| child.parent_id == Some(node.id))
|
||||
let child_ids = inner
|
||||
.children_index
|
||||
.get(&node.id)
|
||||
.cloned()
|
||||
.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| {
|
||||
if inner.is_leak(&child.name) {
|
||||
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.index_add_child(parent.id, new_id);
|
||||
inner.mark_dirty();
|
||||
|
||||
Ok(())
|
||||
@@ -864,17 +1180,20 @@ impl DavFileSystem for CarrierFs {
|
||||
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
|
||||
.manifest
|
||||
.inodes
|
||||
.values()
|
||||
.any(|child| child.parent_id == Some(node.id));
|
||||
.children_index
|
||||
.get(&node.id)
|
||||
.is_some_and(|children| !children.is_empty());
|
||||
|
||||
if has_children {
|
||||
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.mark_dirty();
|
||||
|
||||
@@ -904,6 +1223,9 @@ impl DavFileSystem for CarrierFs {
|
||||
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.mark_dirty();
|
||||
|
||||
@@ -948,6 +1270,10 @@ impl DavFileSystem for CarrierFs {
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -956,8 +1282,15 @@ impl DavFileSystem for CarrierFs {
|
||||
.map(|d| d.as_secs())
|
||||
.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) {
|
||||
inode.parent_id = Some(to_parent.id);
|
||||
inode.parent_id = Some(new_parent_id);
|
||||
inode.name = to_name.to_string();
|
||||
inode.modified_at = now;
|
||||
}
|
||||
@@ -1013,6 +1346,10 @@ impl DavFileSystem for CarrierFs {
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1091,6 +1428,7 @@ impl DavFileSystem for CarrierFs {
|
||||
};
|
||||
|
||||
inner.manifest.inodes.insert(new_id, new_inode);
|
||||
inner.index_add_child(to_parent.id, new_id);
|
||||
inner.mark_dirty();
|
||||
|
||||
Ok(())
|
||||
|
||||
+15
-8
@@ -423,17 +423,24 @@ pub async fn mount_container(
|
||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
||||
}
|
||||
if vault_id == 1 {
|
||||
if let Some((len, cap, pct)) = fs_clone.carrier_manifest_usage() {
|
||||
if len >= crate::carrier::CARRIER_MANIFEST_WARN_THRESHOLD {
|
||||
if let Some(corrupted) = fs_clone.carrier_corrupted_pages() {
|
||||
if corrupted > 0 {
|
||||
println!(
|
||||
" {} Warnung (C-04): Hidden-Vault Manifest zu {:.1}% gefüllt ({}/{} Bytes).",
|
||||
ui::yellow("[!]"),
|
||||
pct,
|
||||
len,
|
||||
cap
|
||||
" {} Warnung (D-01): {} beschädigte Inode-Tabellenseite(n) im Hidden Vault übersprungen!",
|
||||
ui::red("[!]"),
|
||||
corrupted
|
||||
);
|
||||
}
|
||||
}
|
||||
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!(
|
||||
" 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();
|
||||
|
||||
// Berechne Blockanzahl (min. 2 Blöcke: Block 0 für Manifest, Block 1+ für Nutzdaten)
|
||||
let total_blocks = c_size.div_ceil(CHUNK_SIZE as u64).max(2) as u32;
|
||||
// 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(3) as u32;
|
||||
|
||||
// Slot 1 (Hidden Vault) einfügen
|
||||
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_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) =
|
||||
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 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());
|
||||
let ct_end_0 = 32 + inner_ct_0.len();
|
||||
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);
|
||||
|
||||
@@ -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) =
|
||||
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 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());
|
||||
let ct_end_1 = 32 + inner_ct_1.len();
|
||||
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);
|
||||
|
||||
@@ -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(
|
||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
||||
@@ -820,7 +874,7 @@ impl Database {
|
||||
OsRng.fill_bytes(&mut dummy_noise);
|
||||
|
||||
conn.execute_batch("BEGIN TRANSACTION;")?;
|
||||
for b in 2..total_blocks {
|
||||
for b in 3..total_blocks {
|
||||
let (ct, nonce, tag) =
|
||||
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,])?;
|
||||
|
||||
+12
@@ -799,6 +799,18 @@ impl SanctumFs {
|
||||
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 {
|
||||
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
|
||||
let err = cfs_loaded.sync_manifest().unwrap_err();
|
||||
let err_str = err.to_string();
|
||||
// In Format V2 (v0.9.0): Die 1-MB-Grenze bricht NICHT mehr ab, sondern allokiert eine zusätzliche Seite
|
||||
cfs_loaded
|
||||
.sync_manifest()
|
||||
.expect("sync_manifest with > 1 MB must succeed in V2 via paged manifest");
|
||||
assert!(
|
||||
err_str.contains("Carrier-Manifest überschreitet die maximale Blockgröße"),
|
||||
"Error message must specify carrier capacity limit: {}",
|
||||
err_str
|
||||
);
|
||||
assert!(
|
||||
err_str.contains("7.000 Dateien/Verzeichnisse"),
|
||||
"Error message must recommend file limit: {}",
|
||||
err_str
|
||||
cfs_loaded.page_count() >= 2,
|
||||
"Page count must expand to at least 2 pages"
|
||||
);
|
||||
|
||||
// Reloaden und prüfen, ob beide Seiten geladen werden
|
||||
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);
|
||||
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