fix(carrier): C-03 — decouple manifest re-encryption with dirty tracking and unmount flush
This commit is contained in:
+75
-10
@@ -277,6 +277,10 @@ 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 manifest_dirty: bool,
|
||||||
|
pub manifest_save_count: u64,
|
||||||
|
pub dirty_ops_count: u32,
|
||||||
|
pub last_manifest_save: u64,
|
||||||
pub last_activity: Arc<AtomicU64>,
|
pub last_activity: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +288,38 @@ impl CarrierFsInner {
|
|||||||
pub fn is_leak(&self, name: &str) -> bool {
|
pub fn is_leak(&self, name: &str) -> bool {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
if self.dirty_ops_count >= 50 {
|
||||||
|
let _ = self.flush_manifest_if_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schreibt das Manifest nur, wenn ungespeicherte Änderungen anstehen (C-03).
|
||||||
|
pub fn flush_manifest_if_dirty(&mut self) -> Result<()> {
|
||||||
|
if self.manifest_dirty {
|
||||||
|
self.save_manifest()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save_manifest(&mut self) -> Result<()> {
|
pub fn save_manifest(&mut self) -> Result<()> {
|
||||||
|
self.manifest_save_count += 1;
|
||||||
|
self.manifest_dirty = false;
|
||||||
|
self.dirty_ops_count = 0;
|
||||||
|
self.last_manifest_save = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
self.manifest.manifest_generation += 1;
|
self.manifest.manifest_generation += 1;
|
||||||
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
||||||
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
||||||
@@ -304,7 +339,7 @@ impl CarrierFsInner {
|
|||||||
|
|
||||||
// C-02: Rollierendes Dual-Block-Manifest.
|
// C-02: Rollierendes Dual-Block-Manifest.
|
||||||
// Gerade Generation -> Block 0, ungerade Generation -> Block 1.
|
// Gerade Generation -> Block 0, ungerade Generation -> Block 1.
|
||||||
let target_block = if self.manifest.manifest_generation % 2 == 0 {
|
let target_block = if self.manifest.manifest_generation.is_multiple_of(2) {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
@@ -375,6 +410,14 @@ impl CarrierFsInner {
|
|||||||
|
|
||||||
impl Drop for CarrierFsInner {
|
impl Drop for CarrierFsInner {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
if self.manifest_dirty {
|
||||||
|
if let Err(e) = self.save_manifest() {
|
||||||
|
error!(
|
||||||
|
"Fehler beim finalen Speichern des Carrier-Manifests im Drop: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
crate::windows::unlock_memory(self.dek_outer.as_ptr(), 32);
|
crate::windows::unlock_memory(self.dek_outer.as_ptr(), 32);
|
||||||
crate::windows::unlock_memory(self.dek_inner.as_ptr(), 32);
|
crate::windows::unlock_memory(self.dek_inner.as_ptr(), 32);
|
||||||
}
|
}
|
||||||
@@ -491,6 +534,10 @@ 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,
|
||||||
|
manifest_dirty: false,
|
||||||
|
manifest_save_count: 0,
|
||||||
|
dirty_ops_count: 0,
|
||||||
|
last_manifest_save: now,
|
||||||
last_activity: Arc::new(AtomicU64::new(now)),
|
last_activity: Arc::new(AtomicU64::new(now)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -519,6 +566,21 @@ impl CarrierFs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sync_manifest(&self) -> Result<()> {
|
||||||
|
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.flush_manifest_if_dirty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manifest_save_count(&self) -> u64 {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.manifest_save_count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_manifest_dirty(&self) -> bool {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.manifest_dirty
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
@@ -597,7 +659,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
n.size = 0;
|
n.size = 0;
|
||||||
n.modified_at = now;
|
n.modified_at = now;
|
||||||
inner.manifest.inodes.insert(n.id, n.clone());
|
inner.manifest.inodes.insert(n.id, n.clone());
|
||||||
let _ = inner.save_manifest();
|
inner.mark_dirty();
|
||||||
}
|
}
|
||||||
n
|
n
|
||||||
}
|
}
|
||||||
@@ -628,7 +690,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_inode.clone());
|
inner.manifest.inodes.insert(new_id, new_inode.clone());
|
||||||
let _ = inner.save_manifest();
|
inner.mark_dirty();
|
||||||
new_inode
|
new_inode
|
||||||
} else {
|
} else {
|
||||||
return Err(FsError::NotFound);
|
return Err(FsError::NotFound);
|
||||||
@@ -750,7 +812,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_dir);
|
inner.manifest.inodes.insert(new_id, new_dir);
|
||||||
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -783,7 +845,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner.manifest.inodes.remove(&node.id);
|
inner.manifest.inodes.remove(&node.id);
|
||||||
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -812,7 +874,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner.manifest.inodes.remove(&node.id);
|
inner.manifest.inodes.remove(&node.id);
|
||||||
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -869,7 +931,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
inode.modified_at = now;
|
inode.modified_at = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -998,7 +1060,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_inode);
|
inner.manifest.inodes.insert(new_id, new_inode);
|
||||||
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
inner.mark_dirty();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -1305,7 +1367,10 @@ impl DavFile for CarrierFile {
|
|||||||
inode.modified_at = now;
|
inode.modified_at = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
inner.mark_dirty();
|
||||||
|
inner
|
||||||
|
.flush_manifest_if_dirty()
|
||||||
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
|
|
||||||
self.meta.size = self.file_size;
|
self.meta.size = self.file_size;
|
||||||
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||||
@@ -1335,7 +1400,7 @@ impl Drop for CarrierFile {
|
|||||||
inode.blocks = self.blocks.clone();
|
inode.blocks = self.blocks.clone();
|
||||||
inode.modified_at = now;
|
inode.modified_at = now;
|
||||||
}
|
}
|
||||||
let _ = inner.save_manifest();
|
inner.mark_dirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ pub async fn mount_container(
|
|||||||
);
|
);
|
||||||
let fs_leak_counter = fs.leak_counter();
|
let fs_leak_counter = fs.leak_counter();
|
||||||
let last_activity = fs.last_activity();
|
let last_activity = fs.last_activity();
|
||||||
|
let fs_clone = fs.clone();
|
||||||
let dav_server = DavHandler::builder()
|
let dav_server = DavHandler::builder()
|
||||||
.filesystem(Box::new(fs))
|
.filesystem(Box::new(fs))
|
||||||
.locksystem(FakeLs::new())
|
.locksystem(FakeLs::new())
|
||||||
@@ -282,6 +283,7 @@ pub async fn mount_container(
|
|||||||
|
|
||||||
// Netzlaufwerk bzw. Verzeichnis einbinden
|
// Netzlaufwerk bzw. Verzeichnis einbinden
|
||||||
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
||||||
|
let _ = fs_clone.sync_carrier_manifest();
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
return Err(e);
|
return Err(e);
|
||||||
@@ -541,6 +543,7 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stealth {
|
if stealth {
|
||||||
|
let _ = fs_clone.sync_carrier_manifest();
|
||||||
let _ = unmount_drive(drive_letter);
|
let _ = unmount_drive(drive_letter);
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
@@ -555,6 +558,7 @@ pub async fn mount_container(
|
|||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
||||||
} else {
|
} else {
|
||||||
|
let _ = fs_clone.sync_carrier_manifest();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
print!(
|
print!(
|
||||||
|
|||||||
+28
-6
@@ -752,13 +752,24 @@ impl Database {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
|
|
||||||
let (outer_ct_0, outer_nonce_0, outer_tag_0) =
|
let (outer_ct_0, outer_nonce_0, outer_tag_0) = crate::crypto::encrypt_chunk(
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext_0, FORMAT_VERSION, 0)?;
|
dek_0,
|
||||||
|
c_id,
|
||||||
|
0,
|
||||||
|
&outer_plaintext_0,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
0,
|
||||||
|
)?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, 0, 0, ?2, ?3, ?4)",
|
VALUES (?1, 0, 0, ?2, ?3, ?4)",
|
||||||
params![c_id, outer_nonce_0.as_slice(), outer_tag_0.as_slice(), outer_ct_0],
|
params![
|
||||||
|
c_id,
|
||||||
|
outer_nonce_0.as_slice(),
|
||||||
|
outer_tag_0.as_slice(),
|
||||||
|
outer_ct_0
|
||||||
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 2. Block 1 schreiben (Redundante Zweitkopie, Gen 0, C-02)
|
// 2. Block 1 schreiben (Redundante Zweitkopie, Gen 0, C-02)
|
||||||
@@ -778,13 +789,24 @@ impl Database {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
|
|
||||||
let (outer_ct_1, outer_nonce_1, outer_tag_1) =
|
let (outer_ct_1, outer_nonce_1, outer_tag_1) = crate::crypto::encrypt_chunk(
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, 1, &outer_plaintext_1, FORMAT_VERSION, 0)?;
|
dek_0,
|
||||||
|
c_id,
|
||||||
|
1,
|
||||||
|
&outer_plaintext_1,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
0,
|
||||||
|
)?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, 1, 0, ?2, ?3, ?4)",
|
VALUES (?1, 1, 0, ?2, ?3, ?4)",
|
||||||
params![c_id, outer_nonce_1.as_slice(), outer_tag_1.as_slice(), outer_ct_1],
|
params![
|
||||||
|
c_id,
|
||||||
|
outer_nonce_1.as_slice(),
|
||||||
|
outer_tag_1.as_slice(),
|
||||||
|
outer_ct_1
|
||||||
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Blöcke 2..total_blocks-1 mit DEK_0 vorallokieren (C-02)
|
// Blöcke 2..total_blocks-1 mit DEK_0 vorallokieren (C-02)
|
||||||
|
|||||||
@@ -788,6 +788,13 @@ impl SanctumFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sync_carrier_manifest(&self) -> Result<()> {
|
||||||
|
if let Some(ref cfs) = self.carrier_fs {
|
||||||
|
cfs.sync_manifest()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn leak_count(&self) -> u64 {
|
pub fn leak_count(&self) -> u64 {
|
||||||
self.leak_counter().load(Ordering::Relaxed)
|
self.leak_counter().load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -993,8 +993,14 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
|||||||
write: true,
|
write: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut file1 = cfs.open(&file1_path, open_write).await.expect("Create file1");
|
let mut file1 = cfs
|
||||||
file1.write_bytes(Bytes::from_static(b"generation 1 content")).await.unwrap();
|
.open(&file1_path, open_write)
|
||||||
|
.await
|
||||||
|
.expect("Create file1");
|
||||||
|
file1
|
||||||
|
.write_bytes(Bytes::from_static(b"generation 1 content"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
file1.flush().await.unwrap();
|
file1.flush().await.unwrap();
|
||||||
drop(file1);
|
drop(file1);
|
||||||
drop(cfs);
|
drop(cfs);
|
||||||
@@ -1017,8 +1023,14 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
|||||||
write: true,
|
write: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut file2 = cfs_reloaded.open(&file2_path, open_write2).await.expect("Create file2");
|
let mut file2 = cfs_reloaded
|
||||||
file2.write_bytes(Bytes::from_static(b"generation 2 content")).await.unwrap();
|
.open(&file2_path, open_write2)
|
||||||
|
.await
|
||||||
|
.expect("Create file2");
|
||||||
|
file2
|
||||||
|
.write_bytes(Bytes::from_static(b"generation 2 content"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
file2.flush().await.unwrap();
|
file2.flush().await.unwrap();
|
||||||
drop(file2);
|
drop(file2);
|
||||||
drop(cfs_reloaded);
|
drop(cfs_reloaded);
|
||||||
@@ -1071,7 +1083,10 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
file3.write_bytes(Bytes::from_static(b"gen 3")).await.unwrap();
|
file3
|
||||||
|
.write_bytes(Bytes::from_static(b"gen 3"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
file3.flush().await.unwrap();
|
file3.flush().await.unwrap();
|
||||||
drop(file3);
|
drop(file3);
|
||||||
drop(cfs_repair);
|
drop(cfs_repair);
|
||||||
@@ -1133,3 +1148,167 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
|||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_c03_manifest_dirty_decoupling_and_unmount_flush() {
|
||||||
|
let path = temp_db_path("c03_dirty");
|
||||||
|
let carrier_size_bytes = 5 * 1024 * 1024; // 5 MB
|
||||||
|
let carrier_name = "carrier_c03.bin";
|
||||||
|
|
||||||
|
let pass_decoy = "DecoyPass2026!";
|
||||||
|
let pass_hidden = "HiddenPass2026!";
|
||||||
|
|
||||||
|
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");
|
||||||
|
let cfs = 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");
|
||||||
|
|
||||||
|
// 1. Unmittelbar nach Mounten: nicht dirty, 0 Saves
|
||||||
|
assert_eq!(cfs.manifest_save_count(), 0);
|
||||||
|
assert!(!cfs.is_manifest_dirty());
|
||||||
|
|
||||||
|
// 2. Metadaten-Operationen: 3 Ordner anlegen
|
||||||
|
let dir1 = DavPath::new("/dir1").unwrap();
|
||||||
|
let dir2 = DavPath::new("/dir2").unwrap();
|
||||||
|
let dir3 = DavPath::new("/dir3").unwrap();
|
||||||
|
cfs.create_dir(&dir1).await.expect("create_dir dir1");
|
||||||
|
cfs.create_dir(&dir2).await.expect("create_dir dir2");
|
||||||
|
cfs.create_dir(&dir3).await.expect("create_dir dir3");
|
||||||
|
|
||||||
|
// C-03 Verifikation: Manifest ist dirty, aber save_count ist IMMER NOCH 0!
|
||||||
|
assert!(
|
||||||
|
cfs.is_manifest_dirty(),
|
||||||
|
"Manifest must be dirty after create_dir"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfs.manifest_save_count(),
|
||||||
|
0,
|
||||||
|
"No synchronous save_manifest on create_dir (C-03 decoupling)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Datei im Verzeichnis erstellen
|
||||||
|
let file_path = DavPath::new("/dir1/note.txt").unwrap();
|
||||||
|
let open_write = OpenOptions {
|
||||||
|
create_new: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut file = cfs.open(&file_path, open_write).await.expect("open create");
|
||||||
|
assert_eq!(
|
||||||
|
cfs.manifest_save_count(),
|
||||||
|
0,
|
||||||
|
"No synchronous save_manifest on file create"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Expliziter Sync über sync_manifest()
|
||||||
|
cfs.sync_manifest().expect("sync_manifest");
|
||||||
|
assert!(
|
||||||
|
!cfs.is_manifest_dirty(),
|
||||||
|
"Manifest must be clean after sync"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfs.manifest_save_count(),
|
||||||
|
1,
|
||||||
|
"Save count incremented to 1 after sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nochmal sync: da nicht dirty, kein unnötiger Save
|
||||||
|
cfs.sync_manifest().expect("sync_manifest 2");
|
||||||
|
assert_eq!(
|
||||||
|
cfs.manifest_save_count(),
|
||||||
|
1,
|
||||||
|
"Save count remains 1 when not dirty"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. In Datei schreiben und flush() ausführen -> führt zu Manifest-Flush
|
||||||
|
file.write_bytes(Bytes::from_static(b"carrier dirty test payload"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
file.flush().await.expect("file flush");
|
||||||
|
assert_eq!(
|
||||||
|
cfs.manifest_save_count(),
|
||||||
|
2,
|
||||||
|
"File flush must flush manifest"
|
||||||
|
);
|
||||||
|
assert!(!cfs.is_manifest_dirty());
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// 5. Weiteren Ordner anlegen (mark_dirty), dann cfs droppen
|
||||||
|
let dir4 = DavPath::new("/dir4").unwrap();
|
||||||
|
cfs.create_dir(&dir4).await.expect("create_dir dir4");
|
||||||
|
assert!(cfs.is_manifest_dirty());
|
||||||
|
assert_eq!(cfs.manifest_save_count(), 2);
|
||||||
|
|
||||||
|
// Drop von CarrierFs (Drop-Garantie bei Unmount)
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// 6. Neu laden und prüfen, dass dir4 persistiert wurde
|
||||||
|
let cfs_reloaded = 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 reload");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cfs_reloaded
|
||||||
|
.read_dir(&dir4, ReadDirMeta::None)
|
||||||
|
.await
|
||||||
|
.is_ok(),
|
||||||
|
"Directory created before drop must exist"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(cfs_reloaded);
|
||||||
|
drop(db);
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user