winkit 0.1.0

Thin checked wrappers over the Win32 an installer needs: elevation, ACLs, services, the Restart Manager, the registry and shortcuts.
use std::path::Path;

use windows::{
    Win32::System::Com::{
        CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx,
        CoUninitialize, IPersistFile,
    },
    Win32::UI::Shell::{IShellLinkW, ShellLink},
    core::{HSTRING, Interface},
};

use crate::error::{Error, Result};

/// Writes a `.lnk`.
///
/// Used for both the desktop and the Start Menu entry. `icon` is optional;
/// without it the shell takes the icon from the target, which is usually what
/// is wanted anyway.
pub fn create_shortcut(
    link: &Path,
    target: &Path,
    arguments: Option<&str>,
    working_directory: Option<&Path>,
    icon: Option<&Path>,
    description: Option<&str>,
) -> Result<()> {
    let _com = Apartment::enter()?;

    let result = (|| -> windows::core::Result<()> {
        let shell_link: IShellLinkW =
            unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)? };

        unsafe { shell_link.SetPath(&HSTRING::from(target.as_os_str()))? };

        if let Some(arguments) = arguments {
            unsafe { shell_link.SetArguments(&HSTRING::from(arguments))? };
        }

        // Without this the shortcut starts in system32, which breaks anything
        // that looks for a file next to itself.
        let working = working_directory
            .map(Path::to_path_buf)
            .or_else(|| target.parent().map(Path::to_path_buf));
        if let Some(working) = working {
            unsafe { shell_link.SetWorkingDirectory(&HSTRING::from(working.as_os_str()))? };
        }

        if let Some(icon) = icon {
            unsafe { shell_link.SetIconLocation(&HSTRING::from(icon.as_os_str()), 0)? };
        }

        if let Some(description) = description {
            unsafe { shell_link.SetDescription(&HSTRING::from(description))? };
        }

        // The link is saved through a second interface on the same object.
        let persist: IPersistFile = shell_link.cast()?;
        unsafe { persist.Save(&HSTRING::from(link.as_os_str()), true)? };
        Ok(())
    })();

    result.map_err(|e| Error::hresult("create_shortcut", e.code().0))
}

/// Initialises COM for as long as it is needed and no longer.
struct Apartment;

impl Apartment {
    fn enter() -> Result<Self> {
        // S_FALSE means this thread was already in a compatible apartment,
        // which is not a failure and still needs the matching uninitialise.
        let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
        if hr.is_err() {
            return Err(Error::hresult("CoInitializeEx", hr.0));
        }
        Ok(Self)
    }
}

impl Drop for Apartment {
    fn drop(&mut self) {
        unsafe { CoUninitialize() };
    }
}