use std::process::Command; use anyhow::{bail, Context, Result}; /// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D'). pub fn find_next_available_drive() -> Result { #[cfg(windows)] { extern "system" { fn GetLogicalDrives() -> u32; } let mask = unsafe { GetLogicalDrives() }; if mask == 0 { bail!("Fehler beim Abfragen der logischen Laufwerke via Win32 API"); } // Suche von 'Z' abwärts bis 'D' (A, B für Diskettenlaufwerke, C für System reservieren) for ch in ('D'..='Z').rev() { let bit_index = (ch as u8) - b'A'; if (mask & (1 << bit_index)) == 0 { return Ok(ch); } } bail!("Kein freier Windows-Laufwerksbuchstabe (D: bis Z:) verfügbar!"); } #[cfg(not(windows))] { Ok('S') } } /// Öffnet das eingebundene Netzlaufwerk direkt im Windows Explorer. pub fn open_in_explorer(drive_char: char) -> Result<()> { let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase()); Command::new("explorer.exe") .arg(&drive_path) .spawn() .with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?; Ok(()) } /// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED). pub fn notify_shell_associations_changed() { #[cfg(windows)] { extern "system" { fn SHChangeNotify( w_event_id: i32, u_flags: u32, dw_item1: *const std::ffi::c_void, dw_item2: *const std::ffi::c_void, ); } const SHCNE_ASSOCCHANGED: i32 = 0x0800_0000; const SHCNF_IDLIST: u32 = 0x0000; unsafe { SHChangeNotify( SHCNE_ASSOCCHANGED, SHCNF_IDLIST, std::ptr::null(), std::ptr::null(), ); } } } /// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte). pub fn register_explorer_integration() -> Result<()> { let current_exe = std::env::current_exe() .context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?; let exe_str = current_exe.display().to_string(); let reg_commands = [ // 1. .sanctum Erweiterung mit ProgID verknüpfen ( r"HKCU\Software\Classes\.sanctum", "", "Sanctum.Container", ), // 2. ProgID Metadaten & Beschreibung ( r"HKCU\Software\Classes\Sanctum.Container", "", "Sanctum Verschlüsselter Container", ), // 3. Icon ( r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon", "", &format!("\"{exe_str}\",0"), ), // 4. Standard-Doppelklick-Aktion: Mount ( r"HKCU\Software\Classes\Sanctum.Container\shell\open", "", "Als Laufwerk einbinden", ), ( r"HKCU\Software\Classes\Sanctum.Container\shell\open\command", "", &format!("\"{exe_str}\" mount --path \"%1\""), ), // 5. Kontextmenü-Aktion: Integritätsprüfung ( r"HKCU\Software\Classes\Sanctum.Container\shell\verify", "", "Integrität prüfen (FSCK)", ), ( r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command", "", &format!("\"{exe_str}\" verify --path \"%1\""), ), // 6. Kontextmenü-Aktion: Header sichern ( r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header", "", "Header sichern", ), ( r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command", "", &format!("\"{exe_str}\" backup-header --path \"%1\""), ), ]; for (key, val_name, val_data) in reg_commands { let mut cmd = Command::new("reg"); cmd.arg("add").arg(key); if val_name.is_empty() { cmd.arg("/ve"); } else { cmd.arg("/v").arg(val_name); } cmd.arg("/d").arg(val_data).arg("/f"); let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!("Registry-Fehler beim Anlegen von {key}: {stderr}"); } } notify_shell_associations_changed(); Ok(()) } /// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU). pub fn unregister_explorer_integration() -> Result<()> { let keys_to_delete = [ r"HKCU\Software\Classes\.sanctum", r"HKCU\Software\Classes\Sanctum.Container", ]; for key in keys_to_delete { let _ = Command::new("reg") .args(["delete", key, "/f"]) .output(); } notify_shell_associations_changed(); Ok(()) } /// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray. #[cfg(windows)] pub fn get_default_system_icon() -> Option { extern "system" { fn LoadIconW(instance: isize, icon_name: *const u16) -> isize; } // IDI_SHIELD = 32518, IDI_APPLICATION = 32512 let icon = unsafe { LoadIconW(0, 32518 as *const u16) }; if icon != 0 { Some(tray_item::IconSource::RawIcon(icon)) } else { let icon_app = unsafe { LoadIconW(0, 32512 as *const u16) }; if icon_app != 0 { Some(tray_item::IconSource::RawIcon(icon_app)) } else { None } } } /// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre. /// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet. pub struct SessionLockGuard { #[cfg(windows)] hwnd: isize, #[cfg(windows)] join_handle: Option>, } #[cfg(windows)] impl Drop for SessionLockGuard { fn drop(&mut self) { if self.hwnd != 0 { unsafe { PostMessageW(self.hwnd, 0x0010 /* WM_CLOSE */, 0, 0); } } if let Ok(mut guard) = SESSION_LOCK_TX.lock() { *guard = None; } if let Some(handle) = self.join_handle.take() { let _ = handle.join(); } } } #[cfg(not(windows))] pub struct SessionLockGuard; #[cfg(windows)] static SESSION_LOCK_TX: std::sync::Mutex>> = std::sync::Mutex::new(None); #[cfg(windows)] #[allow(non_snake_case)] #[repr(C)] struct WNDCLASSEXW { cbSize: u32, style: u32, lpfnWndProc: Option isize>, cbClsExtra: i32, cbWndExtra: i32, hInstance: isize, hIcon: isize, hCursor: isize, hbrBackground: isize, lpszMenuName: *const u16, lpszClassName: *const u16, hIconSm: isize, } #[cfg(windows)] #[allow(non_snake_case)] #[repr(C)] struct MSG { hwnd: isize, message: u32, wParam: usize, lParam: isize, time: u32, pt_x: i32, pt_y: i32, } #[cfg(windows)] extern "system" { fn GetModuleHandleW(lpModuleName: *const u16) -> isize; fn RegisterClassExW(lpwcx: *const WNDCLASSEXW) -> u16; fn UnregisterClassW(lpClassName: *const u16, hInstance: isize) -> i32; fn CreateWindowExW( dwExStyle: u32, lpClassName: *const u16, lpWindowName: *const u16, dwStyle: u32, x: i32, y: i32, nWidth: i32, nHeight: i32, hWndParent: isize, hMenu: isize, hInstance: isize, lpParam: *mut std::ffi::c_void, ) -> isize; fn DestroyWindow(hwnd: isize) -> i32; fn DefWindowProcW(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> isize; fn GetMessageW(lpMsg: *mut MSG, hWnd: isize, wMsgFilterMin: u32, wMsgFilterMax: u32) -> i32; fn TranslateMessage(lpMsg: *const MSG) -> i32; fn DispatchMessageW(lpMsg: *const MSG) -> isize; fn PostMessageW(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> i32; fn PostQuitMessage(nExitCode: i32); } #[cfg(windows)] #[link(name = "wtsapi32")] extern "system" { fn WTSRegisterSessionNotification(hwnd: isize, flags: u32) -> i32; fn WTSUnRegisterSessionNotification(hwnd: isize) -> i32; } #[cfg(windows)] unsafe extern "system" fn session_wnd_proc( hwnd: isize, msg: u32, wparam: usize, lparam: isize, ) -> isize { const WM_CLOSE: u32 = 0x0010; const WM_WTSSESSION_CHANGE: u32 = 0x02B1; const WTS_SESSION_LOCK: usize = 0x7; const WTS_SESSION_LOGOFF: usize = 0x6; match msg { WM_WTSSESSION_CHANGE => { if wparam == WTS_SESSION_LOCK || wparam == WTS_SESSION_LOGOFF { if let Ok(guard) = SESSION_LOCK_TX.lock() { if let Some(ref tx) = *guard { let _ = tx.blocking_send(()); } } } 0 } WM_CLOSE => { PostQuitMessage(0); 0 } _ => DefWindowProcW(hwnd, msg, wparam, lparam), } } /// Startet einen Hintergrundthread mit einem verdeckten Win32-Nachrichtenfenster (HWND_MESSAGE), /// das via `WTSRegisterSessionNotification` auf Sperr-Events (Win + L) lauscht und beim Eintreffen /// ein Signal an den übergebenen Tokio-Kanal sendet. #[cfg(windows)] pub fn start_session_lock_monitor( shutdown_tx: tokio::sync::mpsc::Sender<()>, ) -> Result { if let Ok(mut guard) = SESSION_LOCK_TX.lock() { *guard = Some(shutdown_tx); } let (hwnd_tx, hwnd_rx) = std::sync::mpsc::channel::(); let join_handle = std::thread::Builder::new() .name("sanctum-session-monitor".to_string()) .spawn(move || unsafe { let h_instance = GetModuleHandleW(std::ptr::null()); let class_name: Vec = "SanctumSessionMonitorClass\0".encode_utf16().collect(); let wcx = WNDCLASSEXW { cbSize: std::mem::size_of::() as u32, style: 0, lpfnWndProc: Some(session_wnd_proc), cbClsExtra: 0, cbWndExtra: 0, hInstance: h_instance, hIcon: 0, hCursor: 0, hbrBackground: 0, lpszMenuName: std::ptr::null(), lpszClassName: class_name.as_ptr(), hIconSm: 0, }; RegisterClassExW(&wcx); let hwnd = CreateWindowExW( 0, class_name.as_ptr(), std::ptr::null(), 0, 0, 0, 0, 0, -3, // HWND_MESSAGE 0, h_instance, std::ptr::null_mut(), ); if hwnd == 0 { let _ = hwnd_tx.send(0); return; } WTSRegisterSessionNotification(hwnd, 0); // NOTIFY_FOR_THIS_SESSION = 0 let _ = hwnd_tx.send(hwnd); let mut msg = std::mem::zeroed::(); while GetMessageW(&mut msg, 0, 0, 0) > 0 { TranslateMessage(&msg); DispatchMessageW(&msg); } WTSUnRegisterSessionNotification(hwnd); DestroyWindow(hwnd); UnregisterClassW(class_name.as_ptr(), h_instance); }) .context("Konnte Windows Session-Monitor-Thread nicht starten")?; let hwnd = hwnd_rx .recv() .map_err(|e| anyhow::anyhow!("Session-Monitor-Thread initialisierte nicht rechtzeitig: {e}"))?; if hwnd == 0 { bail!("Win32-Nachrichtenfenster für Session-Lock konnte nicht erstellt werden"); } Ok(SessionLockGuard { hwnd, join_handle: Some(join_handle), }) } #[cfg(not(windows))] pub fn start_session_lock_monitor( _shutdown_tx: tokio::sync::mpsc::Sender<()>, ) -> Result { Ok(SessionLockGuard) } #[cfg(test)] mod tests { use super::*; #[test] fn test_find_next_available_drive() { let drive = find_next_available_drive().expect("Find next drive"); assert!(drive.is_ascii_alphabetic()); assert!(drive >= 'D' && drive <= 'Z'); } #[test] fn test_session_lock_monitor_lifecycle() { let (tx, _rx) = tokio::sync::mpsc::channel(1); let monitor = start_session_lock_monitor(tx); assert!(monitor.is_ok()); // Dropping monitor closes message loop and joins thread cleanly drop(monitor); } }