use std::ffi::OsStr;
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
#[cfg(any(feature = "recycle-bin", feature = "shell", feature = "shortcuts"))]
use std::path::Path;
#[cfg(any(feature = "recycle-bin", feature = "shortcuts"))]
use std::thread;
#[cfg(any(feature = "elevation", feature = "shell"))]
use windows::core::PCWSTR;
#[cfg(any(feature = "elevation", feature = "shell"))]
use windows::Win32::Foundation::HWND;
#[cfg(any(feature = "recycle-bin", feature = "shortcuts"))]
use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED};
#[cfg(any(feature = "elevation", feature = "shell"))]
use windows::Win32::UI::Shell::ShellExecuteW;
#[cfg(any(feature = "elevation", feature = "shell"))]
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
#[cfg(any(
feature = "elevation",
feature = "recycle-bin",
feature = "shell",
feature = "shortcuts"
))]
use crate::error::{Error, Result};
#[cfg(any(feature = "recycle-bin", feature = "shortcuts"))]
pub(crate) struct ComApartment;
#[cfg(any(feature = "recycle-bin", feature = "shortcuts"))]
impl ComApartment {
pub(crate) fn initialize_sta(context: &'static str) -> Result<Self> {
let result = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
if result.is_ok() {
Ok(Self)
} else {
Err(Error::WindowsApi {
context,
code: result.0,
})
}
}
}
#[cfg(any(feature = "recycle-bin", feature = "shortcuts"))]
impl Drop for ComApartment {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
#[cfg(any(
feature = "elevation",
feature = "recycle-bin",
feature = "shell",
feature = "shortcuts"
))]
pub(crate) fn to_wide_os(value: &OsStr) -> Vec<u16> {
value.encode_wide().chain(std::iter::once(0)).collect()
}
#[cfg(any(
feature = "elevation",
feature = "instance",
feature = "shell",
feature = "shortcuts"
))]
pub(crate) fn to_wide_str(value: &str) -> Vec<u16> {
OsStr::new(value)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
#[cfg(any(
feature = "elevation",
feature = "recycle-bin",
feature = "shell",
feature = "shortcuts"
))]
pub(crate) fn os_str_contains_nul(value: &OsStr) -> bool {
value.encode_wide().any(|unit| unit == 0)
}
#[cfg(any(feature = "recycle-bin", feature = "shell", feature = "shortcuts"))]
pub(crate) fn path_contains_nul(path: &Path) -> bool {
os_str_contains_nul(path.as_os_str())
}
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
pub(crate) fn quote_arg(arg: &OsStr) -> String {
let text = arg.to_string_lossy();
let mut quoted = String::with_capacity(text.len() + 2);
let mut trailing_backslashes = 0usize;
quoted.push('"');
for ch in text.chars() {
match ch {
'\\' => trailing_backslashes += 1,
'"' => {
for _ in 0..(trailing_backslashes * 2 + 1) {
quoted.push('\\');
}
quoted.push('"');
trailing_backslashes = 0;
}
_ => {
for _ in 0..trailing_backslashes {
quoted.push('\\');
}
quoted.push(ch);
trailing_backslashes = 0;
}
}
}
for _ in 0..(trailing_backslashes * 2) {
quoted.push('\\');
}
quoted.push('"');
quoted
}
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
pub(crate) fn join_quoted_args(args: &[OsString]) -> String {
args.iter()
.map(|arg| quote_arg(arg.as_os_str()))
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(any(feature = "elevation", feature = "shell"))]
pub(crate) fn normalize_nonempty_str<'a>(
value: &'a str,
empty_message: &'static str,
nul_message: &'static str,
) -> Result<&'a str> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(Error::InvalidInput(empty_message));
}
if trimmed.contains('\0') {
return Err(Error::InvalidInput(nul_message));
}
Ok(trimmed)
}
#[cfg(any(feature = "elevation", feature = "shell"))]
pub(crate) fn shell_execute(
verb: &str,
target: &OsStr,
parameters: Option<&str>,
context: &'static str,
) -> Result<()> {
let verb_w = to_wide_str(verb);
let target_w = to_wide_os(target);
let parameters_w = parameters.map(to_wide_str);
let parameters_ptr = parameters_w
.as_ref()
.map_or(PCWSTR::null(), |parameters| PCWSTR(parameters.as_ptr()));
let result = unsafe {
ShellExecuteW(
Some(HWND::default()),
PCWSTR(verb_w.as_ptr()),
PCWSTR(target_w.as_ptr()),
parameters_ptr,
PCWSTR::null(),
SW_SHOWNORMAL,
)
};
let code = result.0 as isize;
if code <= 32 {
Err(Error::WindowsApi {
context,
code: code as i32,
})
} else {
Ok(())
}
}
#[cfg(any(feature = "recycle-bin", feature = "shortcuts"))]
pub(crate) fn run_in_sta<T, F>(panic_message: &'static str, work: F) -> Result<T>
where
T: Send + 'static,
F: FnOnce() -> Result<T> + Send + 'static,
{
match thread::spawn(work).join() {
Ok(result) => result,
Err(_) => Err(Error::Unsupported(panic_message)),
}
}
#[cfg(test)]
mod tests {
#[cfg(any(feature = "elevation", feature = "shell"))]
use super::normalize_nonempty_str;
#[cfg(any(
feature = "elevation",
feature = "recycle-bin",
feature = "shell",
feature = "shortcuts"
))]
use super::os_str_contains_nul;
#[cfg(any(
feature = "elevation",
feature = "recycle-bin",
feature = "shell",
feature = "shortcuts"
))]
use super::to_wide_os;
#[cfg(any(
feature = "elevation",
feature = "instance",
feature = "shell",
feature = "shortcuts"
))]
use super::to_wide_str;
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
use super::{join_quoted_args, quote_arg};
#[cfg(all(
not(any(feature = "elevation", feature = "shortcuts")),
any(feature = "recycle-bin", feature = "shell")
))]
use std::ffi::OsStr;
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
use std::ffi::{OsStr, OsString};
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
#[test]
fn quote_arg_handles_empty_spaces_quotes_and_trailing_backslashes() {
let cases = [
("", r#""""#),
("alpha", r#""alpha""#),
("two words", r#""two words""#),
("say \"hi\"", r#""say \"hi\"""#),
(r"C:\Program Files\demo\", r#""C:\Program Files\demo\\""#),
(
r#"C:\path with "quotes"\bin"#,
r#""C:\path with \"quotes\"\bin""#,
),
];
for (input, expected) in cases {
assert_eq!(quote_arg(OsStr::new(input)), expected);
}
}
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
#[test]
fn join_quoted_args_preserves_argument_boundaries() {
let args = [
OsString::from("alpha"),
OsString::from("two words"),
OsString::from(""),
OsString::from("quoted \"text\""),
OsString::from(r"trailing\"),
];
let joined = join_quoted_args(&args);
assert_eq!(joined.matches('"').count() % 2, 0);
assert_eq!(joined.split("\" \"").count(), args.len());
assert_eq!(
joined,
r#""alpha" "two words" "" "quoted \"text\"" "trailing\\""#,
);
}
#[cfg(any(feature = "elevation", feature = "shortcuts"))]
#[test]
fn os_str_contains_nul_detects_embedded_nul() {
assert!(!os_str_contains_nul(OsStr::new("alpha")));
assert!(os_str_contains_nul(OsStr::new("alpha\0beta")));
}
#[cfg(any(feature = "elevation", feature = "shell"))]
#[test]
fn normalize_nonempty_str_trims_and_rejects_invalid_inputs() {
assert_eq!(
normalize_nonempty_str(" open ", "empty", "nul").unwrap(),
"open",
);
assert!(matches!(
normalize_nonempty_str(" ", "empty", "nul"),
Err(crate::Error::InvalidInput("empty"))
));
assert!(matches!(
normalize_nonempty_str("a\0b", "empty", "nul"),
Err(crate::Error::InvalidInput("nul"))
));
}
#[cfg(any(
feature = "elevation",
feature = "instance",
feature = "shell",
feature = "shortcuts"
))]
#[test]
fn to_wide_str_appends_one_terminating_nul() {
let wide = to_wide_str("abc");
assert_eq!(wide, [97, 98, 99, 0]);
}
#[cfg(any(
feature = "elevation",
feature = "recycle-bin",
feature = "shell",
feature = "shortcuts"
))]
#[test]
fn to_wide_os_appends_one_terminating_nul() {
let wide = to_wide_os(OsStr::new("abc"));
assert_eq!(wide, [97, 98, 99, 0]);
}
}