use std::fs::{self, File}; use std::io::Write; use std::path::Path; use sanctum::crypto::{ derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION, }; use sanctum::storage::Database; use sanctum::sync::{run_sync, SyncDirection, SyncOptions}; fn create_test_container(path: &Path) -> (Database, [u8; 32]) { if path.exists() { let _ = fs::remove_file(path); } let db = Database::open(path).expect("Open database"); let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: sanctum::crypto::MIN_MEMORY_COST_KIB, time_cost: sanctum::crypto::MIN_TIME_COST, parallelism: 1, }; let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap(); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap(); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag) .unwrap(); (db, *dek) } #[test] fn test_sync_push_and_pull_basic() { let temp_root = std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let source_dir = temp_root.join("source"); let target_dir = temp_root.join("restored"); fs::create_dir_all(&source_dir).unwrap(); fs::create_dir_all(source_dir.join("sub")).unwrap(); // Testdateien anlegen fs::write(source_dir.join("file1.txt"), b"Hello Sanctum Sync!").unwrap(); fs::write( source_dir.join("sub").join("file2.bin"), vec![0x42u8; 100_000], ) .unwrap(); let (db, dek) = create_test_container(&container_path); // 1. Push let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Backup", &opts, ) .expect("Sync push"); assert_eq!(stats.files_scanned, 2); assert_eq!(stats.files_transferred, 2); assert_eq!(stats.files_skipped, 0); // 2. Erneuter Push (Fast check / Delta): Muss 0 übertragen, 2 überspringen let stats_delta = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Backup", &opts, ) .expect("Sync push delta"); assert_eq!(stats_delta.files_transferred, 0); assert_eq!(stats_delta.files_skipped, 2); // 3. Dry-Run mit neuer Datei fs::write(source_dir.join("new_file.txt"), b"Brand new file").unwrap(); let mut dry_opts = opts.clone(); dry_opts.dry_run = true; let stats_dry = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Backup", &dry_opts, ) .expect("Sync push dry-run"); assert_eq!(stats_dry.files_transferred, 1); assert_eq!(stats_dry.files_skipped, 2); // Verifizieren, dass new_file.txt im Tresor tatsächlich NICHT existiert let node = db .resolve_path_in_vault("/Backup/new_file.txt", 0, &dek) .unwrap(); assert!(node.is_none()); // 4. Pull let mut pull_opts = SyncOptions::default(); pull_opts.direction = SyncDirection::Pull; pull_opts.quiet = true; let stats_pull = run_sync( &db, 0, &dek, FORMAT_VERSION, "/Backup", target_dir.to_str().unwrap(), &pull_opts, ) .expect("Sync pull"); assert_eq!(stats_pull.files_transferred, 2); // Inhalt vergleichen let c1 = fs::read(target_dir.join("file1.txt")).unwrap(); assert_eq!(c1, b"Hello Sanctum Sync!"); let c2 = fs::read(target_dir.join("sub").join("file2.bin")).unwrap(); assert_eq!(c2, vec![0x42u8; 100_000]); // Aufräumen let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_sync_multi_megabyte_large_file() { let temp_root = std::env::temp_dir().join(format!("sanctum_sync_large_{}", rand::random::())); let container_path = temp_root.join("test_large.sanctum"); let source_dir = temp_root.join("source"); let target_dir = temp_root.join("restored"); fs::create_dir_all(&source_dir).unwrap(); // 3.5 MB große Datei erzeugen (geht über 4 Chunks: 0, 1, 2, 3) let large_file_path = source_dir.join("large_payload.bin"); let mut large_file = File::create(&large_file_path).unwrap(); let chunk_sample = vec![0xA5u8; CHUNK_SIZE]; for _ in 0..3 { large_file.write_all(&chunk_sample).unwrap(); } large_file.write_all(&vec![0x5Au8; 512 * 1024]).unwrap(); // 3.5 MB large_file.flush().unwrap(); drop(large_file); let (db, dek) = create_test_container(&container_path); // Push let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/LargeTest", &opts, ) .expect("Sync push large"); assert_eq!(stats.files_transferred, 1); assert_eq!( stats.bytes_transferred, 3 * (CHUNK_SIZE as u64) + 512 * 1024 ); // Pull let mut pull_opts = SyncOptions::default(); pull_opts.direction = SyncDirection::Pull; pull_opts.quiet = true; run_sync( &db, 0, &dek, FORMAT_VERSION, "/LargeTest", target_dir.to_str().unwrap(), &pull_opts, ) .expect("Sync pull large"); let restored = fs::read(target_dir.join("large_payload.bin")).unwrap(); let original = fs::read(&large_file_path).unwrap(); assert_eq!(restored.len(), original.len()); assert_eq!(restored, original); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_sync_delete_and_exclude_flags() { let temp_root = std::env::temp_dir().join(format!("sanctum_sync_flags_{}", rand::random::())); let container_path = temp_root.join("test_flags.sanctum"); let source_dir = temp_root.join("source"); fs::create_dir_all(&source_dir).unwrap(); fs::write(source_dir.join("keep.txt"), b"Keep this").unwrap(); fs::write(source_dir.join("remove_me.txt"), b"Will be deleted later").unwrap(); fs::write(source_dir.join("ignore.tmp"), b"Temporary file").unwrap(); let (db, dek) = create_test_container(&container_path); let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.exclude_patterns = vec!["*.tmp".to_string()]; opts.quiet = true; // 1. Initialer Push mit Exclude let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Files", &opts, ) .unwrap(); assert_eq!(stats.files_transferred, 2); // keep.txt und remove_me.txt assert!(db .resolve_path_in_vault("/Files/ignore.tmp", 0, &dek) .unwrap() .is_none()); // 2. Lokale Datei löschen und Sync mit --delete ausführen fs::remove_file(source_dir.join("remove_me.txt")).unwrap(); opts.delete = true; let stats_del = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Files", &opts, ) .unwrap(); assert_eq!(stats_del.files_deleted, 1); assert!(db .resolve_path_in_vault("/Files/remove_me.txt", 0, &dek) .unwrap() .is_none()); assert!(db .resolve_path_in_vault("/Files/keep.txt", 0, &dek) .unwrap() .is_some()); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s01_delete_preserves_excluded_files_and_directories() { let temp_root = std::env::temp_dir().join(format!("sanctum_s01_test_{}", rand::random::())); let container_path = temp_root.join("test_s01.sanctum"); let source_dir = temp_root.join("source"); fs::create_dir_all(&source_dir).unwrap(); fs::create_dir_all(source_dir.join("excluded_dir")).unwrap(); fs::write(source_dir.join("normal.txt"), b"Normal file").unwrap(); fs::write(source_dir.join("document.pdf"), b"Important PDF").unwrap(); fs::write( source_dir.join("excluded_dir").join("subfile.txt"), b"Subfile in excluded dir", ) .unwrap(); let (db, dek) = create_test_container(&container_path); // 1. Initialer Push ALLER Dateien (ohne Exclude) let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Data", &opts, ) .unwrap(); assert!(db .resolve_path_in_vault("/Data/document.pdf", 0, &dek) .unwrap() .is_some()); assert!(db .resolve_path_in_vault("/Data/excluded_dir/subfile.txt", 0, &dek) .unwrap() .is_some()); // 2. Lokale Kopie von document.pdf und excluded_dir entfernen fs::remove_file(source_dir.join("document.pdf")).unwrap(); fs::remove_dir_all(source_dir.join("excluded_dir")).unwrap(); // 3. Sync Push mit --delete aber MIT --exclude "*.pdf" und --exclude "excluded_dir*" opts.delete = true; opts.delete_excluded = false; opts.exclude_patterns = vec!["*.pdf".to_string(), "excluded_dir*".to_string()]; let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Data", &opts, ) .unwrap(); assert_eq!( stats.files_deleted, 0, "Ausgeschlossene Dateien/Ordner dürfen NICHT gelöscht werden!" ); // PDF und excluded_dir Teilbaum müssen im Tresor überlebt haben! assert!( db.resolve_path_in_vault("/Data/document.pdf", 0, &dek) .unwrap() .is_some(), "PDF im Tresor muss überleben" ); assert!( db.resolve_path_in_vault("/Data/excluded_dir/subfile.txt", 0, &dek) .unwrap() .is_some(), "Teilbaum in excluded_dir muss überleben" ); // 4. Jetzt Push mit --delete UND --delete-excluded opts.delete_excluded = true; let stats_del = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Data", &opts, ) .unwrap(); assert!( stats_del.files_deleted >= 2, "Mit --delete-excluded müssen die verwaisten ausgeschlossenen Dateien gelöscht werden" ); assert!( db.resolve_path_in_vault("/Data/document.pdf", 0, &dek) .unwrap() .is_none(), "PDF muss mit --delete-excluded gelöscht sein" ); assert!( db.resolve_path_in_vault("/Data/excluded_dir/subfile.txt", 0, &dek) .unwrap() .is_none(), "excluded_dir Inhalt muss gelöscht sein" ); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s01_pull_delete_preserves_local_excluded_and_leak_files() { let temp_root = std::env::temp_dir().join(format!("sanctum_s01_pull_{}", rand::random::())); let container_path = temp_root.join("test_s01_pull.sanctum"); let restore_dir = temp_root.join("restored"); fs::create_dir_all(&restore_dir).unwrap(); // Lokale Dateien anlegen, die im Vault NICHT existieren: folder.jpg (Leak file) und local_notes.pdf (Ausschluss) fs::write( restore_dir.join("folder.jpg"), b"Album Art or Explorer Cache", ) .unwrap(); fs::write(restore_dir.join("local_notes.pdf"), b"Private Local Notes").unwrap(); let (db, dek) = create_test_container(&container_path); // Eine Datei im Vault anlegen let root_node = db.get_node_by_id_in_vault(1, 0, &dek).unwrap().unwrap(); let _ = db .create_node_in_vault(0, root_node.id, "vault_file.txt", false, &dek) .unwrap(); // Pull mit --delete und --exclude "*.pdf" let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Pull; opts.delete = true; opts.delete_excluded = false; opts.exclude_patterns = vec!["*.pdf".to_string()]; opts.quiet = true; let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, "/", restore_dir.to_str().unwrap(), &opts, ) .unwrap(); assert_eq!( stats.files_deleted, 0, "folder.jpg und local_notes.pdf dürfen bei Pull mit --delete NICHT gelöscht werden!" ); assert!( restore_dir.join("folder.jpg").exists(), "folder.jpg muss auf dem Host erhalten bleiben" ); assert!( restore_dir.join("local_notes.pdf").exists(), "local_notes.pdf muss auf dem Host erhalten bleiben" ); // Pull mit --delete UND --delete-excluded opts.delete_excluded = true; run_sync( &db, 0, &dek, FORMAT_VERSION, "/", restore_dir.to_str().unwrap(), &opts, ) .unwrap(); assert!( !restore_dir.join("local_notes.pdf").exists(), "local_notes.pdf muss mit --delete-excluded gelöscht werden" ); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s02_platform_independent_path_construction_and_traversal_rejection() { let temp_root = std::env::temp_dir().join(format!("sanctum_s02_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let source_dir = temp_root.join("source"); let target_dir = temp_root.join("restored"); fs::create_dir_all(&source_dir).unwrap(); let deep_dir = source_dir.join("level1").join("level2").join("level3"); fs::create_dir_all(&deep_dir).unwrap(); fs::write(deep_dir.join("deep_doc.txt"), b"Deeply nested content").unwrap(); let (db, dek) = create_test_container(&container_path); // 1. Push: Übertrage mehrstufige Verzeichnisstruktur in den Tresor let mut push_opts = SyncOptions::default(); push_opts.direction = SyncDirection::Push; push_opts.quiet = true; run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/MultiLevel", &push_opts, ) .expect("Sync push multi-level"); // 2. Pull: Prüfe, dass mehrstufige Pfade plattformunabhängig rekonstruiert werden let mut pull_opts = SyncOptions::default(); pull_opts.direction = SyncDirection::Pull; pull_opts.quiet = true; run_sync( &db, 0, &dek, FORMAT_VERSION, "/MultiLevel", target_dir.to_str().unwrap(), &pull_opts, ) .expect("Sync pull multi-level"); let restored_file = target_dir .join("level1") .join("level2") .join("level3") .join("deep_doc.txt"); assert!( restored_file.exists(), "Mehrstufige Datei muss korrekt auf dem Host angelegt worden sein" ); assert_eq!(fs::read(restored_file).unwrap(), b"Deeply nested content"); // 3. Traversal-Abwehr: Bösartiger Knoten mit relativem Ausbruchsversuch // Simuliere manipulierten Knoten im Tresor { let conn = rusqlite::Connection::open(&container_path).unwrap(); // Erstelle Knoten mit manipuliertem Namen '../evil.txt' conn.execute( "INSERT INTO nodes (id, parent_id, name, is_dir, size, created_at, modified_at, is_carrier) VALUES (9999, 1, '../evil.txt', 0, 10, 100, 100, 0)", [], ) .unwrap(); } let malicious_target = temp_root.join("malicious_target"); fs::create_dir_all(&malicious_target).unwrap(); let pull_res = run_sync( &db, 0, &dek, FORMAT_VERSION, "/", malicious_target.to_str().unwrap(), &pull_opts, ); assert!( pull_res.is_err(), "Sync Pull muss bei manipuliertem Traversal-Pfad abbrechen" ); let err_msg = pull_res.unwrap_err().to_string(); assert!( err_msg.contains("Path traversal") || err_msg.contains("Ungültiger Dateiname") || err_msg.contains("unzulässige Trennzeichen"), "Fehlermeldung muss Traversal-Erkennung ausweisen: {}", err_msg ); assert!( !temp_root.join("evil.txt").exists(), "Es darf niemals eine Datei außerhalb des Zielverzeichnisses erzeugt werden" ); // Aufräumen let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s03_symlink_skipping_and_cycle_protection() { let temp_root = std::env::temp_dir().join(format!("sanctum_s03_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let source_dir = temp_root.join("source"); let sub_dir = source_dir.join("sub"); fs::create_dir_all(&sub_dir).unwrap(); fs::write(source_dir.join("regular.txt"), b"Regular content").unwrap(); // Erzeuge eine rekursive Verknüpfung (Junction unter Windows / Symlink unter Unix) let loop_path = sub_dir.join("cycle"); #[cfg(windows)] { let status = std::process::Command::new("powershell") .args([ "-Command", &format!( "New-Item -ItemType Junction -Path '{}' -Target '{}'", loop_path.display(), source_dir.display() ), ]) .status(); if status.is_err() || !status.unwrap().success() { eprintln!("Junction creation skipped (not supported in current environment)"); let _ = fs::remove_dir_all(&temp_root); return; } } #[cfg(unix)] { if let Err(e) = std::os::unix::fs::symlink(&source_dir, &loop_path) { eprintln!("Symlink creation skipped: {e}"); let _ = fs::remove_dir_all(&temp_root); return; } } let (db, dek) = create_test_container(&container_path); let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; // Ohne S-03 Fix würde der Sync endlos in die Schleife laufen und mit Stack-Overflow abstürzen. // Mit Fix wird der Symlink übersprungen und gezählt. let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/SafePush", &opts, ) .expect("Sync push must succeed despite symlink cycle"); assert_eq!( stats.files_transferred, 1, "Nur reguläre Datei darf übertragen werden" ); assert!( stats.files_skipped_symlinks >= 1, "Symlink/Junction muss erkannt und in files_skipped_symlinks gezählt werden" ); // Aufräumen: Unter Windows Junction vor dem remove_dir_all entfernen #[cfg(windows)] { let _ = std::process::Command::new("powershell") .args([ "-Command", &format!("[System.IO.Directory]::Delete('{}')", loop_path.display()), ]) .status(); } let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s04_toctou_file_size_uses_bytes_written() { let temp_root = std::env::temp_dir().join(format!("sanctum_s04_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let file_path = temp_root.join("race_file.bin"); fs::create_dir_all(&temp_root).unwrap(); let initial_data = vec![0xABu8; 12345]; fs::write(&file_path, &initial_data).unwrap(); let (db, dek) = create_test_container(&container_path); // Sync file to vault let res = sanctum::sync::sync_single_file_to_vault( &db, 0, &dek, FORMAT_VERSION, &file_path, 1, "race_file.bin", false, false, ) .expect("Sync single file to vault"); match res { sanctum::sync::FileTransferResult::Transferred { size } => { assert_eq!(size, 12345); } _ => panic!("Expected FileTransferResult::Transferred"), } let node = db .resolve_path_in_vault("/race_file.bin", 0, &dek) .unwrap() .expect("Node must exist"); assert_eq!( node.size, 12345, "Knotengröße muss exakt bytes_written entsprechen" ); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s05_zero_byte_file_truncate_removes_all_chunks() { let temp_root = std::env::temp_dir().join(format!("sanctum_s05_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let file_path = temp_root.join("truncate_file.bin"); fs::create_dir_all(&temp_root).unwrap(); // 1. Zuerst große Datei mit 2.5 Chunks (> 2 MB) anlegen let large_payload = vec![0xEEu8; 2_500_000]; fs::write(&file_path, &large_payload).unwrap(); let (db, dek) = create_test_container(&container_path); // Initialer Push sanctum::sync::sync_single_file_to_vault( &db, 0, &dek, FORMAT_VERSION, &file_path, 1, "truncate_file.bin", false, false, ) .expect("Initial push of large file"); let node = db .resolve_path_in_vault("/truncate_file.bin", 0, &dek) .unwrap() .expect("Node must exist"); assert_eq!(node.size, 2_500_000); // Prüfe, dass Chunks existieren (3 Chunks: 0, 1, 2) { let conn = rusqlite::Connection::open(&container_path).unwrap(); let count: i64 = conn .query_row( "SELECT COUNT(*) FROM chunks WHERE node_id = ?1", rusqlite::params![node.id], |r| r.get(0), ) .unwrap(); assert_eq!(count, 3, "Datei muss 3 Chunks besitzen"); } // 2. Jetzt: Lokale Datei durch 0-Byte Datei ersetzen (Truncate auf 0 Bytes) std::thread::sleep(std::time::Duration::from_millis(1100)); // Timestamp ändern fs::write(&file_path, b"").unwrap(); let res = sanctum::sync::sync_single_file_to_vault( &db, 0, &dek, FORMAT_VERSION, &file_path, 1, "truncate_file.bin", false, false, ) .expect("Push of 0-byte file"); match res { sanctum::sync::FileTransferResult::Transferred { size } => { assert_eq!(size, 0); } _ => panic!("Expected Transferred {{ size: 0 }}"), } // 3. Verifiziere: In SQLite darf KEIN EINZIGER Chunk mehr für diesen Knoten existieren! // (Ohne den S-05 Fix blieb Chunk 0 fälschlicherweise in der Tabelle zurück) { let conn = rusqlite::Connection::open(&container_path).unwrap(); let count: i64 = conn .query_row( "SELECT COUNT(*) FROM chunks WHERE node_id = ?1", rusqlite::params![node.id], |r| r.get(0), ) .unwrap(); assert_eq!( count, 0, "Bei 0-Byte Datei müssen ALLE Chunks (inkl. Chunk 0) gelöscht sein (S-05)" ); } let updated_node = db .resolve_path_in_vault("/truncate_file.bin", 0, &dek) .unwrap() .expect("Node must exist"); assert_eq!(updated_node.size, 0); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_s06_advisory_lock_blocks_sync_and_force_overrides() { let temp_root = std::env::temp_dir().join(format!("sanctum_s06_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let source_dir = temp_root.join("source"); fs::create_dir_all(&source_dir).unwrap(); fs::write(source_dir.join("test_lock.txt"), b"Locked content").unwrap(); let (db, dek) = create_test_container(&container_path); // 1. Initial: Kein Lock aktiv assert!(db.check_advisory_lock().unwrap().is_none()); // 2. Lock setzen (simuliert laufenden Mount) db.acquire_advisory_lock(false) .expect("Acquire advisory lock"); let active_lock = db.check_advisory_lock().unwrap(); assert!(active_lock.is_some()); let (pid, _host, _time) = active_lock.unwrap(); assert_eq!(pid, std::process::id()); let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; opts.force = false; // 3. Sync ohne --force muss mit Lock-Fehler abbrechen let sync_res = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/", &opts, ); assert!( sync_res.is_err(), "Sync ohne --force muss bei aktivem Advisory Lock abbrechen" ); let err_msg = sync_res.unwrap_err().to_string(); assert!( err_msg.contains("Container ist gesperrt"), "Fehlermeldung muss Lock-Hinweis enthalten: {}", err_msg ); // 4. Sync mit --force muss trotz Lock erfolgreich durchlaufen opts.force = true; let sync_force_res = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/", &opts, ); assert!( sync_force_res.is_ok(), "Sync mit --force muss trotz aktivem Advisory Lock gelingen" ); // 5. Lock freigeben db.release_advisory_lock().expect("Release advisory lock"); assert!(db.check_advisory_lock().unwrap().is_none()); // 6. Sync ohne --force gelingt jetzt wieder opts.force = false; let sync_unlocked = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/", &opts, ); assert!(sync_unlocked.is_ok()); // 7. Stale Lock Test: Lock mit nicht mehr existierender PID wird automatisch bereinigt { let conn = rusqlite::Connection::open(&container_path).unwrap(); let current_host = std::env::var("COMPUTERNAME") .or_else(|_| std::env::var("HOSTNAME")) .unwrap_or_else(|_| "localhost".to_string()); conn.execute( "UPDATE meta SET lock_pid = 99999999, lock_host = ?1, lock_time = 1000 WHERE slot_id = 0", rusqlite::params![current_host], ) .unwrap(); } // check_advisory_lock erkennt den toten Prozess und räumt den Lock auf assert!( db.check_advisory_lock().unwrap().is_none(), "Stale Lock eines toten Prozesses muss automatisch als None bewertet werden" ); let sync_stale = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/", &opts, ); assert!( sync_stale.is_ok(), "Sync muss bei verwaistem Lock eines toten Prozesses ohne --force gelingen" ); let _ = fs::remove_dir_all(&temp_root); }