use std::{
borrow::Cow,
io,
path::{Path, PathBuf},
ptr,
};
use num_enum::FromPrimitive;
use widestring::U16CString;
use windows::{
Win32::{
System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, IPersistFile, STGM},
UI::Shell::{IShellLinkW, SLGP_RAWPATH, ShellLink},
},
core::{Interface, PCWSTR},
};
#[cfg(feature = "runas")]
use crate::runas;
use crate::{
Hotkey, Icon, Result, WindowState,
buf_utils::{com_get_optional_path, com_get_optional_string},
com::{self, ComResultExt},
};
#[derive(Debug, Clone, Default)]
#[allow(clippy::unsafe_derive_deserialize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Shortcut {
pub target_path: Option<PathBuf>,
pub arguments: Option<String>,
pub working_dir: Option<PathBuf>,
pub description: Option<String>,
pub icon: Option<Icon>,
pub window_state: WindowState,
pub hotkey: Option<Hotkey>,
#[cfg(feature = "runas")]
pub run_as_admin: bool,
}
impl Shortcut {
#[must_use]
pub fn builder() -> ShortcutBuilder {
ShortcutBuilder::default()
}
#[must_use]
pub fn new(target_path: impl Into<PathBuf>) -> Self {
let target_path = target_path.into();
Self {
target_path: Some(target_path.clone()),
working_dir: target_path.parent().map(|p| p.to_path_buf()),
icon: Some(Icon {
path: target_path,
index: 0,
}),
..Default::default()
}
}
pub fn canonicalize(&mut self, base: Option<&Path>) -> io::Result<()> {
canonicalize_with_base_inplace_opt(self.target_path.as_mut(), base)?;
canonicalize_with_base_inplace_opt(self.working_dir.as_mut(), base)?;
canonicalize_with_base_inplace_opt(self.icon.as_mut().map(|i| &mut i.path), base)?;
Ok(())
}
pub fn canonicalized(&self, base: Option<&Path>) -> io::Result<Self> {
let mut clone = self.clone();
clone.canonicalize(base)?;
Ok(clone)
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
com::ensure_initialized()?;
let link: IShellLinkW = unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER) }
.context(None, "CoCreateInstance")?;
let persist: IPersistFile = link.cast().context(Some("IUnknown"), "QueryInterface")?;
let path = dunce::canonicalize(path)?;
let wpath = U16CString::from_os_str(path.as_os_str())?;
unsafe { persist.Load(PCWSTR(wpath.as_ptr()), STGM(0)) }
.context(Some("IPersistFile"), "Load")?;
let target_path = com_get_optional_path(|b| {
unsafe { link.GetPath(b, ptr::null_mut(), SLGP_RAWPATH.0 as _) }
.context(Some("IShellLinkW"), "GetPath")
})?;
let arguments = com_get_optional_string(|b| {
unsafe { link.GetArguments(b) }.context(Some("IShellLinkW"), "GetArguments")
})?;
let working_dir = com_get_optional_path(|b| {
unsafe { link.GetWorkingDirectory(b) }
.context(Some("IShellLinkW"), "GetWorkingDirectory")
})?;
let description = com_get_optional_string(|b| {
unsafe { link.GetDescription(b) }.context(Some("IShellLinkW"), "GetDescription")
})?;
let mut icon_index = 0;
let icon_path = com_get_optional_path(|b| {
unsafe { link.GetIconLocation(b, &raw mut icon_index) }
.context(Some("IShellLinkW"), "GetIconLocation")
})?;
let icon = icon_path.map(Icon::new);
let window_state_raw =
unsafe { link.GetShowCmd() }.context(Some("IShellLinkW"), "GetShowCmd")?;
let window_state = WindowState::from_primitive(window_state_raw.0);
let hotkey_raw = unsafe { link.GetHotkey() }.context(Some("IShellLinkW"), "GetHotkey")?;
let hotkey = Hotkey::from_raw(hotkey_raw);
#[cfg(feature = "runas")]
let run_as_admin = runas::read_runas_bit(&path)?;
let shortcut = Shortcut {
target_path,
arguments,
working_dir,
description,
icon,
window_state,
hotkey,
#[cfg(feature = "runas")]
run_as_admin,
};
Ok(shortcut)
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
com::ensure_initialized()?;
let path = try_canonicalize(path.as_ref());
let link: IShellLinkW = unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER) }
.context(None, "CoCreateInstance")?;
if let Some(target_path) = &self.target_path {
let w = U16CString::from_os_str(target_path.as_os_str())?;
unsafe { link.SetPath(PCWSTR(w.as_ptr())) }.context(Some("IShellLinkW"), "SetPath")?;
}
if let Some(arguments) = &self.arguments {
let w = U16CString::from_str(arguments)?;
unsafe { link.SetArguments(PCWSTR(w.as_ptr())) }
.context(Some("IShellLinkW"), "SetArguments")?;
}
if let Some(working_dir) = &self.working_dir {
let w = U16CString::from_os_str(working_dir.as_os_str())?;
unsafe { link.SetWorkingDirectory(PCWSTR(w.as_ptr())) }
.context(Some("IShellLinkW"), "SetWorkingDirectory")?;
}
if let Some(description) = &self.description {
let w = U16CString::from_str(description)?;
unsafe { link.SetDescription(PCWSTR(w.as_ptr())) }
.context(Some("IShellLinkW"), "SetDescription")?;
}
if let Some(icon) = &self.icon {
let w = U16CString::from_os_str(icon.path.as_os_str())?;
unsafe { link.SetIconLocation(PCWSTR(w.as_ptr()), icon.index) }
.context(Some("IShellLinkW"), "SetIconLocation")?;
}
unsafe { link.SetShowCmd(self.window_state.into()) }
.context(Some("IShellLinkW"), "SetShowCmd")?;
unsafe { link.SetHotkey(self.hotkey.map_or(0, |h| h.to_raw())) }
.context(Some("IShellLinkW"), "SetHotkey")?;
let persist: IPersistFile = link.cast().context(Some("IUnknown"), "QueryInterface")?;
let wout = U16CString::from_os_str(path.as_os_str())?;
unsafe { persist.Save(PCWSTR(wout.as_ptr()), true) }
.context(Some("IPersistFile"), "Save")?;
unsafe { persist.SaveCompleted(PCWSTR(wout.as_ptr())) }
.context(Some("IPersistFile"), "SaveCompleted")?;
#[cfg(feature = "runas")]
if self.run_as_admin {
runas::write_runas_bit(&path, true)?;
}
Ok(())
}
}
fn cmp_path(a: &Path, b: &Path) -> bool {
try_canonicalize(a)
.as_os_str()
.eq_ignore_ascii_case(try_canonicalize(b).as_os_str())
}
#[allow(clippy::ref_option)]
fn cmp_opt_path(a: &Option<PathBuf>, b: &Option<PathBuf>) -> bool {
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => cmp_path(a.as_path(), b.as_path()),
_ => false,
}
}
impl PartialEq for Shortcut {
fn eq(&self, other: &Self) -> bool {
if !cmp_opt_path(&self.target_path, &other.target_path) {
return false;
}
if self.arguments != other.arguments {
return false;
}
if !cmp_opt_path(&self.working_dir, &other.working_dir) {
return false;
}
if self.description != other.description {
return false;
}
if self.icon != other.icon {
return false;
}
if self.window_state != other.window_state {
return false;
}
if self.hotkey != other.hotkey {
return false;
}
#[cfg(feature = "runas")]
if self.run_as_admin != other.run_as_admin {
return false;
}
true
}
}
#[derive(Debug, Clone, Default)]
pub struct ShortcutBuilder {
inner: Shortcut,
}
impl ShortcutBuilder {
#[must_use]
pub fn new(target_path: impl Into<PathBuf>) -> Self {
Self {
inner: Shortcut::new(target_path),
}
}
#[must_use]
pub fn arguments(mut self, args: impl Into<String>) -> Self {
self.inner.arguments = Some(args.into());
self
}
#[must_use]
pub fn working_dir(mut self, wd: impl Into<PathBuf>) -> Self {
self.inner.working_dir = Some(wd.into());
self
}
#[must_use]
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.inner.description = Some(desc.into());
self
}
#[must_use]
pub fn icon(mut self, icon: Icon) -> Self {
self.inner.icon = Some(icon);
self
}
#[must_use]
pub fn window_state(mut self, state: WindowState) -> Self {
self.inner.window_state = state;
self
}
#[must_use]
pub fn hotkey(mut self, hotkey: Hotkey) -> Self {
self.inner.hotkey = Some(hotkey);
self
}
#[must_use]
pub fn build(self) -> Shortcut {
self.inner
}
}
fn canonicalize_inplace(path: &mut PathBuf) -> io::Result<()> {
*path = dunce::canonicalize(&path)?;
Ok(())
}
#[allow(clippy::unnecessary_unwrap)]
fn canonicalize_with_base_inplace(path: &mut PathBuf, base: Option<&Path>) -> io::Result<()> {
if path.is_absolute() || base.is_none() {
canonicalize_inplace(path)
} else {
let mut absolute = base.unwrap().join(&path);
canonicalize_inplace(&mut absolute)?;
*path = absolute;
Ok(())
}
}
fn canonicalize_with_base_inplace_opt(
opt: Option<&mut PathBuf>,
base: Option<&Path>,
) -> io::Result<()> {
if let Some(path) = opt {
canonicalize_with_base_inplace(path, base)
} else {
Ok(())
}
}
fn try_canonicalize(path: &'_ Path) -> Cow<'_, Path> {
dunce::canonicalize(path).map_or_else(|_| Cow::Borrowed(path), Cow::Owned)
}