From 151fdba09df6900270481f15b6bbf3d6053d91ed Mon Sep 17 00:00:00 2001 From: harald Date: Sat, 19 Sep 2026 00:16:04 +0200 Subject: [PATCH] =?UTF-8?q?fix(vfs):=20V-01=20=E2=80=94=20copy=20dispatche?= =?UTF-8?q?s=20to=20CarrierFs=20in=20carrier=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/carrier.rs | 128 ++++++++++++++++++++++ src/vfs.rs | 8 ++ tests/carrier_model_a_test.rs | 199 ++++++++++++++++++++++++++++++++++ 3 files changed, 335 insertions(+) diff --git a/src/carrier.rs b/src/carrier.rs index e56cc40..85cc2a4 100644 --- a/src/carrier.rs +++ b/src/carrier.rs @@ -747,6 +747,134 @@ impl DavFileSystem for CarrierFs { Ok(()) }) } + + fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { + Box::pin(async move { + self.touch(); + let from_str = Self::path_to_str(from); + let to_str = Self::path_to_str(to); + + if from_str == to_str { + return Ok(()); + } + + let mut inner = self.inner.lock().unwrap(); + let node = inner.resolve_path(&from_str).ok_or(FsError::NotFound)?; + + if node.is_dir { + return Err(FsError::NotImplemented); + } + + let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str); + if inner.anti_leak && is_leak_file(to_name) { + return Err(FsError::Forbidden); + } + + let to_parent = inner + .resolve_path(to_parent_path) + .ok_or(FsError::NotFound)?; + if !to_parent.is_dir { + return Err(FsError::Forbidden); + } + + // Falls Zieldatei existiert und ein Verzeichnis ist: Fehler + if let Some(dest) = inner.resolve_path(&to_str) { + if dest.is_dir { + return Err(FsError::Forbidden); + } + for b in dest.blocks { + let _ = shred_carrier_block( + &inner.db, + inner.carrier_node_id, + b, + &inner.dek_outer, + inner.format_version, + ); + inner.manifest.free_blocks.push(b); + } + inner.manifest.inodes.remove(&dest.id); + } + + if inner.manifest.free_blocks.len() < node.blocks.len() { + return Err(FsError::InsufficientStorage); + } + + let mut dest_blocks = Vec::with_capacity(node.blocks.len()); + for &src_block_idx in &node.blocks { + let dest_block_idx = match inner.allocate_block() { + Ok(b) => b, + Err(e) => { + for b in dest_blocks { + let _ = inner.free_block(b); + } + return Err(e); + } + }; + + let data = match read_carrier_block( + &inner.db, + inner.carrier_node_id, + src_block_idx, + &inner.dek_outer, + &inner.dek_inner, + inner.format_version, + ) { + Ok(d) => d, + Err(e) => { + error!("Fehler beim Lesen des Quell-Blocks während copy: {e}"); + let _ = inner.free_block(dest_block_idx); + for b in dest_blocks { + let _ = inner.free_block(b); + } + return Err(FsError::GeneralFailure); + } + }; + + if let Err(e) = write_carrier_block( + &inner.db, + inner.carrier_node_id, + dest_block_idx, + &inner.dek_outer, + &inner.dek_inner, + &data, + inner.format_version, + ) { + error!("Fehler beim Schreiben des Ziel-Blocks während copy: {e}"); + let _ = inner.free_block(dest_block_idx); + for b in dest_blocks { + let _ = inner.free_block(b); + } + return Err(FsError::GeneralFailure); + } + + dest_blocks.push(dest_block_idx); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + let new_id = inner.manifest.next_inode_id; + inner.manifest.next_inode_id += 1; + + let new_inode = CarrierInode { + id: new_id, + parent_id: Some(to_parent.id), + name: to_name.to_string(), + is_dir: false, + size: node.size, + created_at: now, + modified_at: now, + blocks: dest_blocks, + }; + + inner.manifest.inodes.insert(new_id, new_inode); + inner.save_manifest().map_err(|_| FsError::GeneralFailure)?; + + Ok(()) + }) + } } /// Datei-Handle für Dateien innerhalb des Carrier-Dateisystems mit Streaming und Chunk-Pufferung. diff --git a/src/vfs.rs b/src/vfs.rs index 1e3bec0..622fd0f 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -952,11 +952,19 @@ impl DavFileSystem for SanctumFs { } fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { + if let Some(ref cfs) = self.carrier_fs { + return cfs.copy(from, to); + } + Box::pin(async move { self.touch(); let from_str = Self::path_to_str(from); let to_str = Self::path_to_str(to); + if from_str == to_str { + return Ok(()); + } + let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?; if node.is_dir { diff --git a/tests/carrier_model_a_test.rs b/tests/carrier_model_a_test.rs index 8a41f8a..9eab424 100644 --- a/tests/carrier_model_a_test.rs +++ b/tests/carrier_model_a_test.rs @@ -575,3 +575,202 @@ async fn test_carrier_file_drop_and_append_mode() { let _ = std::fs::remove_file(&path); } + +#[tokio::test] +async fn test_v01_carrier_fs_copy_delegation_and_functionality() { + let path = temp_db_path("v01_copy"); + let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB + let carrier_name = "carrier_for_copy.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 hidden_fs = SanctumFs::with_carrier( + db.clone(), + auth_hidden.dek().clone(), + auth_hidden.carrier_dek(), + auth_hidden.carrier_node_id(), + auth_hidden.version(), + true, + 1, + ); + + // Ordner anlegen + let folder = DavPath::new("/work").unwrap(); + hidden_fs.create_dir(&folder).await.expect("Create folder"); + + // Datei anlegen und Inhalt schreiben + let src_path = DavPath::new("/work/original.txt").unwrap(); + let test_data = b"V01 CarrierFs Copy Functionality Test Payload 2026"; + let mut file = hidden_fs + .open( + &src_path, + OpenOptions { + create: true, + write: true, + ..Default::default() + }, + ) + .await + .expect("Create source file"); + file.write_bytes(Bytes::from_static(test_data)) + .await + .expect("Write source data"); + file.flush().await.expect("Flush source file"); + drop(file); + + // V-01: copy von /work/original.txt nach /work/copy.txt aufrufen + let dest_path = DavPath::new("/work/copy.txt").unwrap(); + hidden_fs + .copy(&src_path, &dest_path) + .await + .expect("V-01: Copy must succeed on CarrierFs"); + + // Metadaten der Kopie prüfen + let copy_meta = hidden_fs + .metadata(&dest_path) + .await + .expect("Metadata of copied file"); + assert_eq!(copy_meta.len(), test_data.len() as u64); + assert!(!copy_meta.is_dir()); + + // Inhalt der Kopie lesen und verifizieren + let mut read_copy = hidden_fs + .open( + &dest_path, + OpenOptions { + read: true, + ..Default::default() + }, + ) + .await + .expect("Open copied file"); + let copy_bytes = read_copy.read_bytes(1024).await.expect("Read copy bytes"); + assert_eq!(©_bytes[..], test_data); + drop(read_copy); + + // Quelldatei löschen: Die Kopie muss unabhängig erhalten bleiben! + hidden_fs + .remove_file(&src_path) + .await + .expect("Delete source file"); + assert!(hidden_fs.metadata(&src_path).await.is_err()); + + let mut read_copy_again = hidden_fs + .open( + &dest_path, + OpenOptions { + read: true, + ..Default::default() + }, + ) + .await + .expect("Open copied file after source delete"); + let copy_bytes_2 = read_copy_again + .read_bytes(1024) + .await + .expect("Read copy bytes after source delete"); + assert_eq!(©_bytes_2[..], test_data); + drop(read_copy_again); + + // Overwrite-Copy testen + let src2_path = DavPath::new("/work/source2.txt").unwrap(); + let test_data_2 = b"New Overwrite Payload Content"; + let mut file2 = hidden_fs + .open( + &src2_path, + OpenOptions { + create: true, + write: true, + ..Default::default() + }, + ) + .await + .expect("Create source2"); + file2 + .write_bytes(Bytes::from_static(test_data_2)) + .await + .expect("Write source2"); + file2.flush().await.expect("Flush source2"); + drop(file2); + + hidden_fs + .copy(&src2_path, &dest_path) + .await + .expect("Overwrite copy must succeed"); + + let mut read_overwritten = hidden_fs + .open( + &dest_path, + OpenOptions { + read: true, + ..Default::default() + }, + ) + .await + .expect("Open overwritten copy"); + let overwritten_bytes = read_overwritten + .read_bytes(1024) + .await + .expect("Read overwritten bytes"); + assert_eq!(&overwritten_bytes[..], test_data_2); + drop(read_overwritten); + + // Checkpoint & Verify + db.checkpoint().unwrap(); + let report = verify_container(&path, Some(&dek_0), true).expect("Verify with DEK_0"); + assert!( + report.is_healthy(), + "Container muss integer und gesund bleiben: {:?}", + report.errors + ); + + let _ = std::fs::remove_file(&path); +} +