autorun 0.1.0

A simple tool to manage autorun entries on Windows
use std::fmt;
use winreg::enums::*;
use winreg::RegKey;

/// Registry path for current-user startup entries.
const RUN_KEY_CU: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
/// Registry path for local-machine startup entries.
const RUN_KEY_LM: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";

/// Registry scope for startup entries.
///
/// `CurrentUser` writes to `HKEY_CURRENT_USER` (no elevation required).
/// `LocalMachine` writes to `HKEY_LOCAL_MACHINE` (requires administrator privileges).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StartupScope {
    /// HKEY_CURRENT_USER — affects the current user only.
    CurrentUser,
    /// HKEY_LOCAL_MACHINE — affects all users; write access needs elevation.
    LocalMachine,
}

impl fmt::Display for StartupScope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            StartupScope::CurrentUser => write!(f, "HKCU"),
            StartupScope::LocalMachine => write!(f, "HKLM"),
        }
    }
}

impl StartupScope {
    /// Open the Run registry key with read + write access.
    fn open_run_key(&self) -> Result<RegKey, String> {
        match self {
            StartupScope::CurrentUser => RegKey::predef(HKEY_CURRENT_USER)
                .open_subkey_with_flags(RUN_KEY_CU, KEY_READ | KEY_WRITE),
            StartupScope::LocalMachine => RegKey::predef(HKEY_LOCAL_MACHINE)
                .open_subkey_with_flags(RUN_KEY_LM, KEY_READ | KEY_WRITE),
        }
        .map_err(|e| format!("failed to open registry key [{}]: {}", self, e))
    }

    /// Open the Run registry key with read-only access.
    fn open_run_key_readonly(&self) -> Result<RegKey, String> {
        match self {
            StartupScope::CurrentUser => RegKey::predef(HKEY_CURRENT_USER)
                .open_subkey_with_flags(RUN_KEY_CU, KEY_READ),
            StartupScope::LocalMachine => RegKey::predef(HKEY_LOCAL_MACHINE)
                .open_subkey_with_flags(RUN_KEY_LM, KEY_READ),
        }
        .map_err(|e| format!("failed to open registry key [{}]: {}", self, e))
    }
}

/// A single startup entry.
#[derive(Debug, Clone)]
pub struct StartupEntry {
    /// The registry value name (identifier for this entry).
    pub name: String,
    /// The command or program path executed at startup.
    pub command: String,
    /// Which registry scope this entry belongs to.
    pub scope: StartupScope,
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// Add a startup entry.
///
/// - `name`  — identifier for the entry (registry value name).
/// - `command` — the program path or command line to run at startup.
/// - `scope` — `CurrentUser` or `LocalMachine`.
///
/// # Note
/// Writing to `LocalMachine` requires **administrator privileges**.
pub fn add(name: &str, command: &str, scope: StartupScope) -> Result<(), String> {
    let key = scope.open_run_key()?;
    key.set_value(name, &command)
        .map_err(|e| format!("failed to add startup entry '{}': {}", name, e))?;
    Ok(())
}

/// Remove a startup entry.
///
/// Returns `Ok(())` even if the entry does not exist (idempotent).
pub fn remove(name: &str, scope: StartupScope) -> Result<(), String> {
    let key = scope.open_run_key()?;
    match key.delete_value(name) {
        Ok(()) => Ok(()),
        Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(e) => Err(format!("failed to remove startup entry '{}': {}", name, e)),
    }
}

/// Check whether a startup entry with the given name exists.
pub fn exists(name: &str, scope: StartupScope) -> Result<bool, String> {
    let key = scope.open_run_key_readonly()?;
    match key.get_value::<String, _>(name) {
        Ok(_) => Ok(true),
        Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(e) => Err(format!("failed to query startup entry '{}': {}", name, e)),
    }
}

/// List all startup entries in the given scope.
pub fn list(scope: StartupScope) -> Result<Vec<StartupEntry>, String> {
    let key = scope.open_run_key_readonly()?;
    let mut entries = Vec::new();

    for name_result in key.enum_values() {
        let (name, value) = name_result
            .map_err(|e| format!("failed to enumerate startup entries: {}", e))?;
        let command = match value {
            winreg::RegValue {
                vtype: REG_SZ | REG_EXPAND_SZ,
                bytes,
            } => String::from_utf16(
                &bytes
                    .chunks_exact(2)
                    .map(|b| u16::from_le_bytes([b[0], b[1]]))
                    .collect::<Vec<_>>(),
            )
            .unwrap_or_else(|_| String::from_utf8_lossy(&bytes).into_owned()),
            _ => continue, // skip non-string registry value types
        };

        entries.push(StartupEntry {
            name,
            command,
            scope,
        });
    }

    Ok(entries)
}

/// List startup entries across **all** scopes (HKCU + HKLM).
///
/// Silently skips `LocalMachine` if the process lacks read permission.
pub fn list_all() -> Result<Vec<StartupEntry>, String> {
    let mut all = Vec::new();

    for scope in [StartupScope::CurrentUser, StartupScope::LocalMachine] {
        match list(scope) {
            Ok(mut entries) => all.append(&mut entries),
            // Silently skip HKLM when lacking privileges.
            Err(_) if scope == StartupScope::LocalMachine => {}
            Err(e) => return Err(e),
        }
    }

    Ok(all)
}