autorun 0.1.1

A simple tool to manage autorun entries on Windows
use std::fmt;
use thiserror::Error;
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";

// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------

/// All errors that can occur in this library.
#[derive(Error, Debug)]
pub enum Error {
    #[error("failed to open registry key [{0}]: {1}")]
    OpenKey(Scope, #[source] std::io::Error),

    #[error("failed to add startup entry '{0}': {1}")]
    Add(String, #[source] std::io::Error),

    #[error("failed to remove startup entry '{0}': {1}")]
    Remove(String, #[source] std::io::Error),

    #[error("failed to query startup entry '{0}': {1}")]
    Query(String, #[source] std::io::Error),

    #[error("failed to enumerate startup entries: {0}")]
    Enumerate(#[source] std::io::Error),
}

// ---------------------------------------------------------------------------
// Scope
// ---------------------------------------------------------------------------

/// Registry scope for startup entries.
///
/// `User` writes to `HKEY_CURRENT_USER` (no elevation required).
/// `Machine` writes to `HKEY_LOCAL_MACHINE` (requires administrator privileges).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Scope {
    /// HKEY_CURRENT_USER — affects the current user only.
    User,
    /// HKEY_LOCAL_MACHINE — affects all users; write access needs elevation.
    Machine,
}

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

impl Scope {
    /// Open the Run registry key with read + write access.
    fn open_run_key(&self) -> Result<RegKey, Error> {
        let (hkey, path) = match self {
            Scope::User => (HKEY_CURRENT_USER, RUN_KEY_CU),
            Scope::Machine => (HKEY_LOCAL_MACHINE, RUN_KEY_LM),
        };
        RegKey::predef(hkey)
            .open_subkey_with_flags(path, KEY_READ | KEY_WRITE)
            .map_err(|e| Error::OpenKey(*self, e))
    }

    /// Open the Run registry key with read-only access.
    fn open_run_key_readonly(&self) -> Result<RegKey, Error> {
        let (hkey, path) = match self {
            Scope::User => (HKEY_CURRENT_USER, RUN_KEY_CU),
            Scope::Machine => (HKEY_LOCAL_MACHINE, RUN_KEY_LM),
        };
        RegKey::predef(hkey)
            .open_subkey_with_flags(path, KEY_READ)
            .map_err(|e| Error::OpenKey(*self, e))
    }

    /// List all startup entries in this scope.
    pub fn list(&self) -> Result<Vec<Entry>, Error> {
        let key = self.open_run_key_readonly()?;
        let mut entries = Vec::new();

        for name_result in key.enum_values() {
            let (name, value) = name_result.map_err(Error::Enumerate)?;
            let command = match value {
                winreg::RegValue {
                    vtype: REG_SZ | REG_EXPAND_SZ,
                    bytes,
                } => String::from_utf16(
                    &bytes
                        .as_chunks::<2>()
                        .0
                        .iter()
                        .map(|b| u16::from_le_bytes([b[0], b[1]]))
                        .collect::<Vec<_>>(),
                )
                .unwrap_or_else(|_| String::from_utf8_lossy(&bytes).into_owned())
                .trim_end_matches('\0')
                .to_owned(),
                _ => continue, // skip non-string registry value types
            };

            entries.push(Entry {
                name,
                command,
                scope: *self,
            });
        }

        Ok(entries)
    }
}

// ---------------------------------------------------------------------------
// Entry
// ---------------------------------------------------------------------------

/// A single startup entry.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Entry {
    /// 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: Scope,
}

impl Entry {
    /// Remove this startup entry from the registry.
    ///
    /// Returns `Ok(())` even if the entry does not exist (idempotent).
    pub fn remove(&self) -> Result<(), Error> {
        crate::remove(&self.name, self.scope)
    }
}

// ---------------------------------------------------------------------------
// 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` — `User` or `Machine`.
///
/// # Note
/// Writing to `Machine` requires **administrator privileges**.
pub fn add(name: &str, command: &str, scope: Scope) -> Result<(), Error> {
    let key = scope.open_run_key()?;
    key.set_value(name, &command)
        .map_err(|e| Error::Add(name.into(), e))?;
    Ok(())
}

/// Remove a startup entry.
///
/// Returns `Ok(())` even if the entry does not exist (idempotent).
pub fn remove(name: &str, scope: Scope) -> Result<(), Error> {
    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(Error::Remove(name.into(), e)),
    }
}

/// Check whether a startup entry with the given name exists.
pub fn exists(name: &str, scope: Scope) -> Result<bool, Error> {
    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(Error::Query(name.into(), e)),
    }
}

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

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

    Ok(all)
}