From 681fb0c032ea9da7885050d339c4222b47fb7c02 Mon Sep 17 00:00:00 2001 From: Harald Date: Mon, 7 Sep 2026 16:01:50 +0200 Subject: [PATCH] Enhance CLI output: UTF-8 console codepage, VT colors, clean status cards --- src/lib.rs | 1 + src/main.rs | 86 +++++++++++++++++++++++++++------------------ src/mount.rs | 72 ++++++++++++++++++++++---------------- src/ui.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 64 deletions(-) create mode 100644 src/ui.rs diff --git a/src/lib.rs b/src/lib.rs index 397b117..cbd0430 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod crypto; pub mod mount; pub mod storage; +pub mod ui; pub mod vfs; diff --git a/src/main.rs b/src/main.rs index a9a3198..ba35875 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,12 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; -use tracing::info; use tracing_subscriber::EnvFilter; use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams}; use sanctum::mount::{format_drive, mount_container, unmount_drive}; use sanctum::storage::Database; +use sanctum::ui; #[derive(Parser)] #[command(name = "sanctum")] @@ -73,10 +73,11 @@ fn handle_init(container_path: &Path) -> Result<()> { ); } - println!("============================================================"); - println!(" Sanctum Container Initialisierung"); - println!(" Zieldatei: {}", container_path.display()); - println!("============================================================"); + println!("┌─────────────────────────────────────────────────────────────┐"); + println!("│ Sanctum — Neuen verschlüsselten Container erstellen │"); + println!("└─────────────────────────────────────────────────────────────┘"); + println!(" Zieldatei: {}", container_path.display()); + println!(); let password = rpassword::prompt_password("Master-Passwort eingeben: ") .context("Fehler beim Einlesen des Passworts")?; @@ -92,20 +93,21 @@ fn handle_init(container_path: &Path) -> Result<()> { bail!("Die eingegebenen Passwörter stimmen nicht überein!"); } - info!("Generiere KDF-Salt und leite KEK via Argon2id ab ..."); + println!(); + ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)..."); let salt = generate_salt(); let kdf_params = KdfParams::default(); let kek = derive_kek(&password, &salt, &kdf_params) .context("KDF-Schlüsselableitung fehlgeschlagen")?; - info!("Erzeuge kryptografisch sicheren DEK via CSPRNG (OsRng) ..."); + ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG..."); let dek = generate_dek(); - info!("Verschlüssele DEK mit KEK via AES-256-GCM ..."); + ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM..."); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?; - info!("Initialisiere SQLite-Containerstruktur mit WAL-Modus ..."); + ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus..."); let db = Database::open(container_path) .context("Konnte SQLite-Containerdatei nicht anlegen")?; @@ -116,31 +118,23 @@ fn handle_init(container_path: &Path) -> Result<()> { .context("Fehler beim finalen WAL-Checkpoint")?; println!(); - println!("✔ Sanctum-Container erfolgreich initialisiert!"); - println!(" Container: {}", container_path.display()); - println!(" Format: Version 1 (Magic: SANCTUM\\0)"); - println!(" KDF: Argon2id (M=64MB, T=3, P=4)"); - println!(" Cipher: AES-256-GCM mit AEAD-Swap-Protection (1-MB Chunks)"); + println!("┌─────────────────────────────────────────────────────────────┐"); + println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │"); + println!("└─────────────────────────────────────────────────────────────┘"); + println!(); + println!(" • Container: {}", container_path.display()); + println!(" • Format: Version 1 (Magic: SANCTUM\\0)"); + println!(" • KDF: Argon2id (M=64MB, T=3, P=4)"); + println!(" • Cipher: AES-256-GCM (1-MB Chunks, AEAD Swap-Schutz)"); + println!(); + println!(" Befehl zum Einbinden als Netzlaufwerk:"); + println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\" --drive S", container_path.display()))); println!(); - println!("Zum Einbinden ausführen:"); - println!( - " sanctum mount --path \"{}\" --drive S", - container_path.display() - ); Ok(()) } -#[tokio::main] -async fn main() -> Result<()> { - // Tracing / Logging initialisieren (Standard-Filter: info) - tracing_subscriber::fmt() - .with_env_filter( - EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("sanctum=info,dav_server=warn,hyper=warn")), - ) - .init(); - +async fn run() -> Result<()> { let cli = Cli::parse(); match cli.command { @@ -149,20 +143,46 @@ async fn main() -> Result<()> { } Commands::Mount { path, drive, port } => { let drive_char = parse_drive_letter(&drive)?; - let password = rpassword::prompt_password(format!( + let prompt_text = format!( "Master-Passwort für Container '{}' eingeben: ", path.display() - )) - .context("Fehler beim Einlesen des Passworts")?; + ); + let password = rpassword::prompt_password(prompt_text) + .context("Fehler beim Einlesen des Passworts")?; mount_container(&path, drive_char, port, &password).await?; } Commands::Unmount { drive } => { let drive_char = parse_drive_letter(&drive)?; + let drive_str = format_drive(drive_char); + print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str); + let _ = std::io::Write::flush(&mut std::io::stdout()); unmount_drive(drive_char)?; - println!("✔ Laufwerk {} erfolgreich getrennt.", format_drive(drive_char)); + println!("{}", ui::green("OK")); + println!("{} Laufwerk {} erfolgreich getrennt.", ui::green("✔"), drive_str); } } Ok(()) } + +#[tokio::main] +async fn main() { + // Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren + ui::init_console(); + + // Tracing / Logging initialisieren (Standard-Filter: warn) + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("sanctum=warn,dav_server=warn,hyper=warn")), + ) + .init(); + + if let Err(err) = run().await { + eprintln!(); + eprintln!("{} {:#}", ui::red("✖ Fehler:"), err); + std::process::exit(1); + } +} + diff --git a/src/mount.rs b/src/mount.rs index 773dc98..ee11e39 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -10,10 +10,11 @@ use hyper::service::service_fn; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; use tokio::sync::watch; -use tracing::{debug, error, info, warn}; +use tracing::{debug, warn}; use crate::crypto::{derive_kek, unwrap_dek}; use crate::storage::Database; +use crate::ui; use crate::vfs::SanctumFs; /// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:") @@ -24,7 +25,6 @@ pub fn format_drive(drive_letter: char) -> String { /// Trennt ein Windows-Netzlaufwerk via `net use : /delete /y`. pub fn unmount_drive(drive_letter: char) -> Result<()> { let drive_str = format_drive(drive_letter); - info!("Trennen des Netzlaufwerks {} ...", drive_str); let output = Command::new("net") .args(["use", &drive_str, "/delete", "/y"]) @@ -42,14 +42,12 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> { ); } - info!("Laufwerk {} erfolgreich getrennt.", drive_str); Ok(()) } /// Bindet ein Windows-Netzlaufwerk via `net use : http://127.0.0.1:/ /persistent:no` ein. fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> { let url = format!("http://127.0.0.1:{}/", port); - info!("Verbinde Netzlaufwerk {} mit {} ...", drive_str, url); let output = Command::new("net") .args(["use", drive_str, &url, "/persistent:no"]) @@ -67,7 +65,6 @@ fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> { ); } - info!("Laufwerk {} erfolgreich eingebunden!", drive_str); Ok(()) } @@ -87,23 +84,20 @@ pub async fn mount_container( ); } - info!( - "Öffne Container '{}' ...", - container_path.display() - ); + println!(); + ui::step(1, 4, "📦", "Öffne Container & verifiziere Header..."); let db = Database::open(container_path) .context("Konnte Container-Datenbank nicht öffnen")?; - info!("Lese Header und verifiziere Magic Bytes ..."); let meta = db .read_meta() .context("Konnte Container-Header nicht lesen")?; - info!("Leite KEK via Argon2id ab ..."); + ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab..."); let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params) .context("Schlüsselableitung fehlgeschlagen")?; - info!("Entschlüssele DEK via AES-256-GCM ..."); + ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM..."); let dek = unwrap_dek( &kek, &meta.wrapped_dek, @@ -124,10 +118,6 @@ pub async fn mount_container( let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await { Ok(l) => l, Err(_) if requested_port.is_none() => { - info!( - "Standard-Port {} belegt, wähle dynamischen freien Port ...", - port_to_try - ); TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))) .await .context("Konnte keinen lokalen TCP-Port binden")? @@ -139,7 +129,16 @@ pub async fn mount_container( let bound_addr = listener.local_addr()?; let bound_port = bound_addr.port(); - info!("WebDAV-Server lauscht auf http://{}", bound_addr); + + ui::step( + 4, + 4, + "🌐", + &format!( + "Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...", + bound_port, drive_str + ), + ); let (shutdown_tx, mut shutdown_rx) = watch::channel(false); @@ -175,7 +174,7 @@ pub async fn mount_container( }); } _ = shutdown_rx.changed() => { - info!("WebDAV-Server-Task empfängt Shutdown-Signal."); + debug!("WebDAV-Server-Task empfängt Shutdown-Signal."); break; } } @@ -190,13 +189,15 @@ pub async fn mount_container( } println!(); - println!("============================================================"); - println!(" Sanctum Container erfolgreich gemountet!"); - println!(" Pfad: {}", container_path.display()); - println!(" Laufwerk: {}", drive_str); - println!(" WebDAV URL: http://127.0.0.1:{}/", bound_port); - println!(" Drücke [Ctrl+C] zum sauberen Trennen und Schließen."); - println!("============================================================"); + println!("┌─────────────────────────────────────────────────────────────┐"); + println!("│ ✔ Sanctum Container erfolgreich gemountet │"); + println!("└─────────────────────────────────────────────────────────────┘"); + println!(); + println!(" • Container: {}", container_path.display()); + println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str)); + println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port); + println!(); + println!(" [{}] Drücke [Ctrl+C] zum sicheren Trennen und Schließen.", ui::yellow("Tipp")); println!(); // Warten auf Strg+C @@ -205,12 +206,15 @@ pub async fn mount_container( .context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?; println!(); - info!("Beendigungssignal (Ctrl+C) erhalten."); - info!("Trennen des Windows-Netzlaufwerks {} ...", drive_str); + println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); + print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str); + let _ = std::io::Write::flush(&mut std::io::stdout()); // Automatisches Unmount if let Err(e) = unmount_drive(drive_letter) { - warn!("Warnung beim automatischen Unmount: {e}"); + println!("{}", ui::yellow(&format!("Warnung ({e})"))); + } else { + println!("{}", ui::green("OK")); } // HTTP Server beenden @@ -218,11 +222,17 @@ pub async fn mount_container( let _ = server_handle.await; // SQLite WAL Checkpoint erzwingen - info!("Führe SQLite WAL-Checkpoint aus (PRAGMA wal_checkpoint(TRUNCATE)) ..."); + print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]")); + let _ = std::io::Write::flush(&mut std::io::stdout()); if let Err(e) = db.checkpoint() { - error!("Fehler beim WAL-Checkpoint: {e}"); + println!("{}", ui::red(&format!("Fehler ({e})"))); + } else { + println!("{}", ui::green("OK")); } - info!("Sanctum Container wurde sicher und vollständig geschlossen."); + println!(); + println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔")); + println!(); Ok(()) } + diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..2b66960 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,99 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +static VT_ENABLED: AtomicBool = AtomicBool::new(false); + +/// Initialisiert die Windows-Konsole: +/// - Setzt Code-Page 65001 (UTF-8) für Ein- und Ausgabe (verhindert Umlaut-Fehler wie `f├╝r`) +/// - Aktiviert VIRTUAL_TERMINAL_PROCESSING für saubere ANSI-Farb- und Escape-Sequenz-Darstellung +#[cfg(windows)] +pub fn init_console() { + const CP_UTF8: u32 = 65001; + const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5; // (DWORD)-11 + const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004; + + extern "system" { + fn SetConsoleOutputCP(wCodePageID: u32) -> i32; + fn SetConsoleCP(wCodePageID: u32) -> i32; + fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void; + fn GetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, lpMode: *mut u32) -> i32; + fn SetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, dwMode: u32) -> i32; + } + + unsafe { + let _ = SetConsoleOutputCP(CP_UTF8); + let _ = SetConsoleCP(CP_UTF8); + + let handle = GetStdHandle(STD_OUTPUT_HANDLE); + if !handle.is_null() && handle as isize != -1 { + let mut mode: u32 = 0; + if GetConsoleMode(handle, &mut mode) != 0 { + if SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 { + VT_ENABLED.store(true, Ordering::Relaxed); + } + } + } + } +} + +#[cfg(not(windows))] +pub fn init_console() { + VT_ENABLED.store(true, Ordering::Relaxed); +} + +pub fn is_vt_enabled() -> bool { + VT_ENABLED.load(Ordering::Relaxed) +} + +pub fn bold(s: &str) -> String { + if is_vt_enabled() { + format!("\x1b[1m{s}\x1b[0m") + } else { + s.to_string() + } +} + +pub fn dim(s: &str) -> String { + if is_vt_enabled() { + format!("\x1b[90m{s}\x1b[0m") + } else { + s.to_string() + } +} + +pub fn green(s: &str) -> String { + if is_vt_enabled() { + format!("\x1b[1;32m{s}\x1b[0m") + } else { + s.to_string() + } +} + +pub fn cyan(s: &str) -> String { + if is_vt_enabled() { + format!("\x1b[1;36m{s}\x1b[0m") + } else { + s.to_string() + } +} + +pub fn yellow(s: &str) -> String { + if is_vt_enabled() { + format!("\x1b[1;33m{s}\x1b[0m") + } else { + s.to_string() + } +} + +pub fn red(s: &str) -> String { + if is_vt_enabled() { + format!("\x1b[1;31m{s}\x1b[0m") + } else { + s.to_string() + } +} + +/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...` +pub fn step(num: u8, total: u8, icon: &str, msg: &str) { + let tag = cyan(&format!("[{}/{}]", num, total)); + println!(" {} {} {}", tag, icon, msg); +}