diff --git a/src/vfs.rs b/src/vfs.rs index 41c5bec..10f6e5c 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -1043,17 +1043,30 @@ impl DavFileSystem for SanctumFs { return Err(FsError::Forbidden); } - // Schutz der Trägerdatei im Decoy Vault: Überschreiben durch Copy verboten! - if let Some(dest) = self.resolve_path(&to_str)? { - if self.carrier_node_id == Some(dest.id) { - return Err(FsError::Forbidden); - } - } - let to_parent = self .resolve_path(to_parent_path)? .ok_or(FsError::NotFound)?; + if !to_parent.is_dir { + return Err(FsError::Forbidden); + } + + // RFC-4918 (V-07): Falls Zieldatei bereits existiert: + // - Wenn Verzeichnis: 403 Forbidden + // - Wenn Datei: bestehende Datei vor dem Kopieren löschen & shredden + if let Some(dest) = self.resolve_path(&to_str)? { + if dest.is_dir { + return Err(FsError::Forbidden); + } + // Schutz der Trägerdatei im Decoy Vault: Überschreiben durch Copy verboten! + if self.carrier_node_id == Some(dest.id) { + return Err(FsError::Forbidden); + } + self.db + .delete_node(dest.id) + .map_err(|_| FsError::GeneralFailure)?; + } + let dest_node = self.create_node(to_parent.id, to_name, false)?; // Kopiere alle Chunks und re-verschlüssele mit neuer node_id (wegen AAD-Bindung!) @@ -1568,4 +1581,58 @@ mod tests { let end_bytes = read_file.read_bytes(3).await.unwrap(); assert_eq!(&end_bytes[..], b"END"); } + + #[tokio::test] + async fn test_v07_copy_overwrite_existing_file_semantics() { + let (fs, _dir) = create_test_fs(true); + let src_path = DavPath::new("/source.txt").unwrap(); + let dest_path = DavPath::new("/dest.txt").unwrap(); + + // 1. Erstelle Quelldatei + let mut opts1 = OpenOptions::default(); + opts1.write = true; + opts1.create_new = true; + let mut src_file = fs.open(&src_path, opts1).await.unwrap(); + let src_data = b"Freshly copied source data (RFC-4918)"; + src_file + .write_bytes(Bytes::copy_from_slice(src_data)) + .await + .unwrap(); + src_file.flush().await.unwrap(); + drop(src_file); + + // 2. Erstelle Zieldatei mit abweichendem Inhalt und Größe + let mut opts2 = OpenOptions::default(); + opts2.write = true; + opts2.create_new = true; + let mut dest_file = fs.open(&dest_path, opts2).await.unwrap(); + let old_dest_data = b"Old obsolete destination data that MUST be overwritten"; + dest_file + .write_bytes(Bytes::copy_from_slice(old_dest_data)) + .await + .unwrap(); + dest_file.flush().await.unwrap(); + drop(dest_file); + + // 3. Kopiere Quelle auf existierendes Ziel (RFC-4918 Copy Overwrite) + fs.copy(&src_path, &dest_path) + .await + .expect("Copy with overwrite must succeed per RFC-4918"); + + // 4. Verifiziere Zielinhalt + let mut read_opts = OpenOptions::default(); + read_opts.read = true; + let mut read_dest = fs.open(&dest_path, read_opts).await.unwrap(); + let read_bytes = read_dest.read_bytes(src_data.len() + 100).await.unwrap(); + assert_eq!(&read_bytes[..], src_data); + + let dest_node = fs.resolve_path("/dest.txt").unwrap().unwrap(); + assert_eq!(dest_node.size, src_data.len() as u64); + + // 5. Test: Kopieren auf existierendes Verzeichnis muss mit Forbidden fehlschlagen + let dir_path = DavPath::new("/sub_dir").unwrap(); + fs.create_dir(&dir_path).await.unwrap(); + let copy_to_dir_res = fs.copy(&src_path, &dir_path).await; + assert!(matches!(copy_to_dir_res, Err(FsError::Forbidden))); + } }