434 lines
13 KiB
Rust
434 lines
13 KiB
Rust
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<char> {
|
|
#[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<tray_item::IconSource> {
|
|
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<std::thread::JoinHandle<()>>,
|
|
}
|
|
|
|
#[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<Option<tokio::sync::mpsc::Sender<()>>> =
|
|
std::sync::Mutex::new(None);
|
|
|
|
#[cfg(windows)]
|
|
#[allow(non_snake_case)]
|
|
#[repr(C)]
|
|
struct WNDCLASSEXW {
|
|
cbSize: u32,
|
|
style: u32,
|
|
lpfnWndProc: Option<unsafe extern "system" fn(isize, u32, usize, isize) -> 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<SessionLockGuard> {
|
|
if let Ok(mut guard) = SESSION_LOCK_TX.lock() {
|
|
*guard = Some(shutdown_tx);
|
|
}
|
|
|
|
let (hwnd_tx, hwnd_rx) = std::sync::mpsc::channel::<isize>();
|
|
|
|
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<u16> = "SanctumSessionMonitorClass\0".encode_utf16().collect();
|
|
|
|
let wcx = WNDCLASSEXW {
|
|
cbSize: std::mem::size_of::<WNDCLASSEXW>() 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::<MSG>();
|
|
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<SessionLockGuard> {
|
|
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);
|
|
}
|
|
}
|