zv 0.15.0

Ziglang Version Manager and Project Starter
use color_eyre::{
    Result,
    config::{HookBuilder, Theme},
};
use tracing_subscriber::prelude::*;

// We only expect to route to `zig` or `zls` once from `zv`
// For example: `zv init --zig`  => `zv` spawns `zig`, +1 in [instantiate_zig]
const ZV_RECURSION_MAX: u32 = 1;

#[tokio::main]
async fn main() -> Result<()> {
    // Apply security mitigations as early as possible
    #[cfg(windows)]
    apply_windows_security_mitigations();

    check_recursion_with_context("zv main")?;

    #[cfg(feature = "dotenv")]
    dotenv::dotenv().ok();

    // Initialize color support
    yansi::whenever(yansi::Condition::TTY_AND_COLOR);

    // Set up error reporting with color-aware themes
    if yansi::is_enabled() {
        HookBuilder::default()
            .display_env_section(cfg!(debug_assertions))
            .display_location_section(cfg!(debug_assertions))
            .install()?;
    } else {
        HookBuilder::default()
            .theme(Theme::new())
            .display_env_section(cfg!(debug_assertions))
            .display_location_section(cfg!(debug_assertions))
            .install()?;
    }

    // Set up tracing with progress bar support
    init_tracing()?;

    let program_name = get_program_name()?;
    match program_name.as_str() {
        "zv" => cli::zv_main().await,
        "zig" => cli::zig_main().await,
        "zls" => cli::zls_main().await,
        _ => {
            eprintln!(
                "Unknown invocation: {}. This binary should be invoked as 'zv', 'zig', or 'zls'.",
                program_name
            );
            std::process::exit(1);
        }
    }
}

/// Initialize tracing with dual-mode logging
///
/// - If ZV_LOG is not set: Simple "info: message" format for user-friendly output  
/// - If ZV_LOG is set: Full structured tracing with timestamps and module paths
fn init_tracing() -> Result<()> {
    let zv_log = std::env::var("ZV_LOG").is_ok();

    if zv_log {
        // Full structured logging mode
        tracing_subscriber::registry()
            .with(
                tracing_subscriber::fmt::layer()
                    .with_target(true) // Show module paths
                    .with_filter(
                        tracing_subscriber::EnvFilter::try_from_env("ZV_LOG")
                            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("zv=warn")),
                    ),
            )
            .init();
    } else {
        // Simple user-friendly logging mode
        tracing_subscriber::registry()
            .with(
                tracing_subscriber::fmt::layer()
                    .with_target(false) // Hide module paths
                    .with_level(true) // Show level
                    .with_thread_ids(false)
                    .with_thread_names(false)
                    .with_file(false)
                    .with_line_number(false)
                    .without_time() // No timestamps
                    .with_filter(tracing_subscriber::EnvFilter::new("zv=info")),
            )
            .init();
    }

    Ok(())
}
fn get_program_name() -> Result<String> {
    // Use args().next() to get the program name as invoked, not the actual executable path
    // This is important for hard links and symlinks to work correctly
    let program_path = std::env::args_os()
        .next()
        .ok_or_else(|| color_eyre::eyre::eyre!("Failed to get program name from args"))?;

    let file_name = std::path::Path::new(&program_path)
        .file_name()
        .ok_or_else(|| color_eyre::eyre::eyre!("Failed to get executable filename"))?
        .to_string_lossy();

    Ok(normalize_program_name(&file_name, cfg!(windows)))
}

fn normalize_program_name(file_name: &str, is_windows: bool) -> String {
    if !is_windows {
        return file_name.to_string();
    }

    let path = std::path::Path::new(file_name);
    let name = if path
        .extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("exe"))
    {
        path.file_stem()
            .and_then(|stem| stem.to_str())
            .unwrap_or(file_name)
    } else {
        file_name
    };

    name.to_ascii_lowercase()
}

/// Apply Windows-specific security mitigations to prevent DLL hijacking
///
/// This function should be called as early as possible in main(), before any
/// dynamic library loading occurs. It restricts DLL loading to trusted system
/// directories only, preventing malicious DLLs from being loaded from the
/// current directory or arbitrary PATH locations.
#[cfg(windows)]
pub fn apply_windows_security_mitigations() {
    use windows_sys::Win32::System::LibraryLoader::{
        LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_LIBRARY_SEARCH_USER_DIRS, SetDefaultDllDirectories,
    };

    // Restrict DLL loading to system directories only
    // This prevents loading DLLs from the current directory or PATH
    let search_flags = LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS;

    unsafe {
        let result = SetDefaultDllDirectories(search_flags);
        // SetDefaultDllDirectories should never fail with valid arguments
        assert_ne!(result, 0, "Failed to set secure DLL directories");
    }

    tracing::debug!("Applied Windows DLL security mitigations");
}

/// Check recursion depth with context for better error messages
pub fn check_recursion_with_context(context: &str) -> Result<()> {
    // Recursion guard - prevent infinite loops but allow zig subcommands such as zv init --zig :  zv -> zig
    let recursion_count = std::env::var("ZV_RECURSION_COUNT")
        .unwrap_or_else(|_| "0".to_string())
        .parse::<u32>()
        .unwrap_or(0);

    if recursion_count > ZV_RECURSION_MAX {
        eprintln!(
            "Error: Too many recursive calls detected in {} (depth: {}). \
             The zv binary may be calling itself infinitely.",
            context, recursion_count
        );
        std::process::exit(1);
    }
    Ok(())
}

mod app;
mod cli;
mod shell;
mod templates;
mod tools;
mod types;

pub use app::App;
pub use shell::*;
pub use templates::*;
pub use types::*;

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

    #[test]
    fn normalizes_windows_exe_extension_case() {
        assert_eq!(normalize_program_name("zig.exe", true), "zig");
        assert_eq!(normalize_program_name("zig.EXE", true), "zig");
        assert_eq!(normalize_program_name("zls.ExE", true), "zls");
        assert_eq!(normalize_program_name("ZV.EXE", true), "zv");
    }

    #[test]
    fn preserves_unix_invocation_case_and_suffix() {
        assert_eq!(normalize_program_name("zig.exe", false), "zig.exe");
        assert_eq!(normalize_program_name("ZIG", false), "ZIG");
    }
}