fix(carrier): C-03 — decouple manifest re-encryption with dirty tracking and unmount flush
This commit is contained in:
@@ -993,8 +993,14 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
||||
write: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut file1 = cfs.open(&file1_path, open_write).await.expect("Create file1");
|
||||
file1.write_bytes(Bytes::from_static(b"generation 1 content")).await.unwrap();
|
||||
let mut file1 = cfs
|
||||
.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();
|
||||
drop(file1);
|
||||
drop(cfs);
|
||||
@@ -1017,8 +1023,14 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
||||
write: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut file2 = cfs_reloaded.open(&file2_path, open_write2).await.expect("Create file2");
|
||||
file2.write_bytes(Bytes::from_static(b"generation 2 content")).await.unwrap();
|
||||
let mut file2 = cfs_reloaded
|
||||
.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();
|
||||
drop(file2);
|
||||
drop(cfs_reloaded);
|
||||
@@ -1071,7 +1083,10 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
||||
)
|
||||
.await
|
||||
.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();
|
||||
drop(file3);
|
||||
drop(cfs_repair);
|
||||
@@ -1133,3 +1148,167 @@ async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
||||
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