sb-vault 0.1.0

S&B Vault // Zero-Trust Desktop Suite & Secret Engine
use std::collections::HashMap;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PortInfo {
    pub port: u16,
    pub is_occupied: bool,
    pub pid: Option<u32>,
    pub process_name: Option<String>,
    pub local_address: Option<String>,
}

/// Holt eine Map von PID -> Prozessname unter Windows via tasklist
#[cfg(windows)]
fn get_process_names_windows() -> HashMap<u32, String> {
    let mut map = HashMap::new();
    if let Ok(output) = std::process::Command::new("tasklist")
        .args(["/FO", "CSV", "/NH"])
        .output()
    {
        let stdout = String::from_utf8_lossy(&output.stdout);
        for line in stdout.lines() {
            let parts: Vec<&str> = line.split(',').collect();
            if parts.len() >= 2 {
                let name = parts[0].trim_matches('"').trim().to_string();
                let pid_str = parts[1].trim_matches('"').trim();
                if let Ok(pid) = pid_str.parse::<u32>() {
                    map.insert(pid, name);
                }
            }
        }
    }
    map
}

/// Scannt eine Liste von TCP-Ports und ermittelt, ob sie belegt sind und von welchem Prozess
#[tauri::command]
pub async fn scan_ports(ports_to_check: Option<Vec<u16>>) -> Result<Vec<PortInfo>, String> {
    let default_ports = vec![3000, 3001, 5173, 5174, 8000, 8080, 5432, 6379, 27017, 3306];
    let ports = ports_to_check.unwrap_or(default_ports);

    #[cfg(windows)]
    {
        let output = tokio::process::Command::new("netstat")
            .args(["-ano", "-p", "tcp"])
            .output()
            .await
            .map_err(|e| format!("Fehler beim Ausführen von netstat: {}", e))?;

        let stdout = String::from_utf8_lossy(&output.stdout);
        let proc_map = get_process_names_windows();

        // Map von Port -> (PID, Local Address)
        let mut occupied_map: HashMap<u16, (u32, String)> = HashMap::new();

        for line in stdout.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            // Format: Proto, Local Address, Foreign Address, State, PID
            // z.B.: TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 14220
            if parts.len() >= 5 && parts[0].eq_ignore_ascii_case("TCP") && parts[3].eq_ignore_ascii_case("LISTENING") {
                let local_addr = parts[1];
                let pid_str = parts[4];
                if let Some(colon_idx) = local_addr.rfind(':') {
                    if let Ok(port) = local_addr[colon_idx + 1..].parse::<u16>() {
                        if let Ok(pid) = pid_str.parse::<u32>() {
                            occupied_map.insert(port, (pid, local_addr.to_string()));
                        }
                    }
                }
            }
        }

        let mut results = Vec::new();
        for port in ports {
            if let Some((pid, addr)) = occupied_map.get(&port) {
                let proc_name = proc_map.get(pid).cloned();
                results.push(PortInfo {
                    port,
                    is_occupied: true,
                    pid: Some(*pid),
                    process_name: proc_name,
                    local_address: Some(addr.clone()),
                });
            } else {
                results.push(PortInfo {
                    port,
                    is_occupied: false,
                    pid: None,
                    process_name: None,
                    local_address: None,
                });
            }
        }

        Ok(results)
    }

    #[cfg(unix)]
    {
        // Auf Unix lsof oder ss verwenden
        let mut results = Vec::new();
        for port in ports {
            let output = tokio::process::Command::new("lsof")
                .args(["-i", &format!(":{}", port), "-sTCP:LISTEN", "-t"])
                .output()
                .await;

            match output {
                Ok(out) if out.status.success() && !out.stdout.is_empty() => {
                    let pid_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
                    let pid = pid_str.lines().next().and_then(|l| l.parse::<u32>().ok());
                    results.push(PortInfo {
                        port,
                        is_occupied: true,
                        pid,
                        process_name: None,
                        local_address: Some(format!("0.0.0.0:{}", port)),
                    });
                }
                _ => {
                    results.push(PortInfo {
                        port,
                        is_occupied: false,
                        pid: None,
                        process_name: None,
                        local_address: None,
                    });
                }
            }
        }
        Ok(results)
    }
}

/// Beendet einen Prozess anhand seiner PID sofort
#[tauri::command]
pub async fn kill_process_by_pid(pid: u32) -> Result<String, String> {
    if pid == 0 || pid == 4 {
        return Err("System-Prozesse (PID 0, 4) können aus Sicherheitsgründen nicht beendet werden.".to_string());
    }

    #[cfg(windows)]
    {
        let output = tokio::process::Command::new("taskkill")
            .args(["/F", "/PID", &pid.to_string()])
            .output()
            .await
            .map_err(|e| format!("Fehler beim Ausführen von taskkill: {}", e))?;

        if output.status.success() {
            Ok(format!("Prozess mit PID {} wurde erfolgreich beendet.", pid))
        } else {
            let err = String::from_utf8_lossy(&output.stderr);
            Err(format!("Prozess konnte nicht beendet werden: {}", err.trim()))
        }
    }

    #[cfg(unix)]
    {
        let output = tokio::process::Command::new("kill")
            .args(["-9", &pid.to_string()])
            .output()
            .await
            .map_err(|e| format!("Fehler beim Beenden: {}", e))?;

        if output.status.success() {
            Ok(format!("Prozess mit PID {} wurde erfolgreich beendet.", pid))
        } else {
            let err = String::from_utf8_lossy(&output.stderr);
            Err(format!("Prozess konnte nicht beendet werden: {}", err.trim()))
        }
    }
}

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

    #[tokio::test]
    async fn test_scan_ports_runs() {
        let res = scan_ports(Some(vec![80, 443, 65530])).await;
        assert!(res.is_ok());
        let list = res.unwrap();
        assert_eq!(list.len(), 3);
    }
}