sb-vault 0.1.0

S&B Vault // Zero-Trust Desktop Suite & Secret Engine
use std::collections::HashMap;
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use base64::Engine;
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, State};

pub struct TerminalSession {
    pub pty_master: Box<dyn MasterPty + Send>,
    pub writer: Box<dyn Write + Send>,
    pub child: Box<dyn Child + Send>,
    pub target: String,
}

#[derive(Default, Clone)]
pub struct TerminalState {
    pub sessions: Arc<Mutex<HashMap<String, TerminalSession>>>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TerminalInfo {
    pub session_id: String,
    pub target: String,
}

/// Startet eine neue interaktive PTY-Sitzung (Lokal oder via sb-ssh zu Server)
#[tauri::command]
pub async fn start_terminal_session(
    target: Option<String>,
    cols: Option<u16>,
    rows: Option<u16>,
    app: AppHandle,
    vault_state: State<'_, crate::vault::VaultState>,
    term_state: State<'_, TerminalState>,
) -> Result<String, String> {
    let target_name = target.unwrap_or_else(|| "local".to_string());

    // Falls ein Server-Target gewählt ist, Vault-Lock prüfen
    if target_name != "local" {
        crate::vault::ensure_unlocked(&vault_state)?;
    }

    let pty_system = native_pty_system();
    let pair = pty_system
        .openpty(PtySize {
            rows: rows.unwrap_or(24),
            cols: cols.unwrap_or(80),
            pixel_width: 0,
            pixel_height: 0,
        })
        .map_err(|e| format!("Konnte PTY nicht öffnen: {}", e))?;

    let cmd = if target_name == "local" {
        #[cfg(windows)]
        {
            let mut c = CommandBuilder::new("powershell.exe");
            c.args(["-NoLogo"]);
            c
        }
        #[cfg(unix)]
        {
            let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
            CommandBuilder::new(shell)
        }
    } else {
        let sb_ssh = which::which("sb-ssh")
            .or_else(|_| {
                dirs::home_dir()
                    .map(|h| {
                        h.join(".cargo").join("bin").join(if cfg!(windows) {
                            "sb-ssh.exe"
                        } else {
                            "sb-ssh"
                        })
                    })
                    .filter(|p| p.exists())
                    .ok_or("Nicht gefunden")
            })
            .map_err(|_| "sb-ssh Binary wurde nicht im System gefunden".to_string())?;

        let mut c = CommandBuilder::new(sb_ssh);
        c.args(["connect", &target_name]);
        c
    };

    let child = pair
        .slave
        .spawn_command(cmd)
        .map_err(|e| format!("Prozess-Start fehlgeschlagen: {}", e))?;

    let mut reader = pair
        .master
        .try_clone_reader()
        .map_err(|e| format!("Konnte PTY Reader nicht klonen: {}", e))?;

    let writer = pair
        .master
        .take_writer()
        .map_err(|e| format!("Konnte PTY Writer nicht entnehmen: {}", e))?;

    let session_id = format!("term-{}", uuid_simple());

    // Reader-Thread im Hintergrund
    let app_handle = app.clone();
    let s_id = session_id.clone();
    std::thread::spawn(move || {
        let mut buffer = [0u8; 4096];
        loop {
            match reader.read(&mut buffer) {
                Ok(0) => break, // EOF
                Ok(n) => {
                    let b64 = base64::engine::general_purpose::STANDARD.encode(&buffer[..n]);
                    let _ = app_handle.emit(&format!("terminal-out-{}", s_id), b64);
                }
                Err(_) => break,
            }
        }
        let _ = app_handle.emit(&format!("terminal-exit-{}", s_id), 0);
    });

    // Session im State registrieren
    {
        let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
        sessions.insert(
            session_id.clone(),
            TerminalSession {
                pty_master: pair.master,
                writer,
                child,
                target: target_name,
            },
        );
    }

    Ok(session_id)
}

/// Schreibt Benutzereingaben in das PTY
#[tauri::command]
pub fn write_terminal_data(
    session_id: String,
    data: String,
    term_state: State<'_, TerminalState>,
) -> Result<(), String> {
    let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
    if let Some(session) = sessions.get_mut(&session_id) {
        session
            .writer
            .write_all(data.as_bytes())
            .map_err(|e| format!("Fehler beim Schreiben ins PTY: {}", e))?;
        session
            .writer
            .flush()
            .map_err(|e| format!("Flush fehlgeschlagen: {}", e))?;
        Ok(())
    } else {
        Err(format!("Terminal-Sitzung '{}' existiert nicht", session_id))
    }
}

/// Passt Zeilen & Spalten des PTY an Fenstergröße an
#[tauri::command]
pub fn resize_terminal(
    session_id: String,
    cols: u16,
    rows: u16,
    term_state: State<'_, TerminalState>,
) -> Result<(), String> {
    let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
    if let Some(session) = sessions.get_mut(&session_id) {
        session
            .pty_master
            .resize(PtySize {
                rows,
                cols,
                pixel_width: 0,
                pixel_height: 0,
            })
            .map_err(|e| format!("Resize fehlgeschlagen: {}", e))?;
        Ok(())
    } else {
        Err(format!("Terminal-Sitzung '{}' nicht gefunden", session_id))
    }
}

/// Schließt eine PTY-Sitzung und beendet den Child-Prozess
#[tauri::command]
pub fn close_terminal_session(
    session_id: String,
    term_state: State<'_, TerminalState>,
) -> Result<(), String> {
    let mut sessions = term_state.sessions.lock().map_err(|e| e.to_string())?;
    if let Some(mut session) = sessions.remove(&session_id) {
        let _ = session.child.kill();
        Ok(())
    } else {
        Ok(())
    }
}

fn uuid_simple() -> String {
    use rand::Rng;
    let mut rng = rand::thread_rng();
    let num: u64 = rng.gen();
    format!("{:x}", num)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pty_system_available() {
        let pty_system = native_pty_system();
        let pair = pty_system.openpty(PtySize {
            rows: 24,
            cols: 80,
            pixel_width: 0,
            pixel_height: 0,
        });
        assert!(pair.is_ok(), "PTY could not be initialized");
    }
}