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_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