Skip to main content

win_desktop_utils/
elevation.rs

1//! Elevation helpers for checking admin state and relaunching through UAC.
2
3use std::ffi::{OsStr, OsString};
4
5use windows::Win32::UI::Shell::IsUserAnAdmin;
6
7use crate::error::{Error, Result};
8use crate::win::{join_quoted_args, normalize_nonempty_str, os_str_contains_nul, shell_execute};
9
10fn join_args_for_shell_execute(args: &[OsString]) -> String {
11    join_quoted_args(args)
12}
13
14fn validate_shell_args(args: &[OsString], message: &'static str) -> Result<()> {
15    if args.iter().any(|arg| os_str_contains_nul(arg.as_os_str())) {
16        return Err(Error::InvalidInput(message));
17    }
18
19    Ok(())
20}
21
22fn validate_restart_args(args: &[OsString]) -> Result<()> {
23    validate_shell_args(args, "restart arguments cannot contain NUL bytes")
24}
25
26fn validate_command_args(args: &[OsString]) -> Result<()> {
27    validate_shell_args(args, "arguments cannot contain NUL bytes")
28}
29
30fn validate_executable(executable: &OsStr) -> Result<()> {
31    if executable.is_empty() {
32        return Err(Error::InvalidInput("executable cannot be empty"));
33    }
34
35    if os_str_contains_nul(executable) {
36        return Err(Error::InvalidInput("executable cannot contain NUL bytes"));
37    }
38
39    Ok(())
40}
41
42fn normalize_shell_verb(verb: &str) -> Result<&str> {
43    normalize_nonempty_str(
44        verb,
45        "verb cannot be empty",
46        "verb cannot contain NUL bytes",
47    )
48}
49
50fn shell_execute_command(
51    verb: &str,
52    executable: &OsStr,
53    args: &[OsString],
54    context: &'static str,
55) -> Result<()> {
56    let joined_args = join_args_for_shell_execute(args);
57    let parameters = if joined_args.is_empty() {
58        None
59    } else {
60        Some(joined_args.as_str())
61    };
62    shell_execute(verb, executable, parameters, context)
63}
64
65/// Returns `true` if the current process is running elevated.
66///
67/// # Errors
68///
69/// This function currently does not produce operational errors, but it returns the
70/// crate's standard [`Result`] type for API consistency.
71///
72/// # Examples
73///
74/// ```no_run
75/// let elevated = win_desktop_utils::is_elevated()?;
76/// println!("elevated: {elevated}");
77/// # Ok::<(), win_desktop_utils::Error>(())
78/// ```
79pub fn is_elevated() -> Result<bool> {
80    let is_admin = unsafe { IsUserAnAdmin() };
81    Ok(is_admin.as_bool())
82}
83
84/// Relaunches the current executable with elevation using the Windows `runas` shell verb.
85///
86/// Arguments are passed as [`OsString`] values so Windows-native argument text is preserved
87/// as well as possible before being joined for `ShellExecuteW` using standard Windows
88/// command-line quoting rules.
89///
90/// This function starts a new elevated instance of the current executable. It does not
91/// terminate the current process.
92///
93/// # Errors
94///
95/// Returns [`Error::Io`] if the current executable path cannot be resolved.
96/// Returns [`Error::InvalidInput`] if any argument contains NUL bytes.
97/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
98///
99/// # Examples
100///
101/// ```no_run
102/// use std::ffi::OsString;
103///
104/// let args = [OsString::from("--help")];
105/// win_desktop_utils::restart_as_admin(&args)?;
106/// # Ok::<(), win_desktop_utils::Error>(())
107/// ```
108pub fn restart_as_admin(args: &[OsString]) -> Result<()> {
109    validate_restart_args(args)?;
110
111    let exe = std::env::current_exe()?;
112    shell_execute_command("runas", exe.as_os_str(), args, "ShellExecuteW(runas)")
113}
114
115/// Launches an executable with elevation using the Windows `runas` shell verb.
116///
117/// Unlike [`restart_as_admin`], this function can start any executable or command
118/// resolvable by the Windows shell.
119///
120/// # Errors
121///
122/// Returns [`Error::InvalidInput`] if `executable` is empty or contains NUL bytes,
123/// or if any argument contains NUL bytes.
124/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
125///
126/// # Examples
127///
128/// ```no_run
129/// use std::ffi::OsString;
130///
131/// let args = [OsString::from("/c"), OsString::from("echo elevated")];
132/// win_desktop_utils::run_as_admin("cmd.exe", &args)?;
133/// # Ok::<(), win_desktop_utils::Error>(())
134/// ```
135pub fn run_as_admin(executable: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
136    run_with_verb("runas", executable, args)
137}
138
139/// Launches an executable through `ShellExecuteW` using an explicit shell verb.
140///
141/// This is useful for verbs such as `open` and `runas` when you need to pass a
142/// command-line argument list.
143///
144/// # Errors
145///
146/// Returns [`Error::InvalidInput`] if `verb` is empty, whitespace only, or contains
147/// NUL bytes, if `executable` is empty or contains NUL bytes, or if any argument
148/// contains NUL bytes.
149/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
150///
151/// # Examples
152///
153/// ```no_run
154/// use std::ffi::OsString;
155///
156/// let args = [OsString::from("--help")];
157/// win_desktop_utils::run_with_verb("open", "notepad.exe", &args)?;
158/// # Ok::<(), win_desktop_utils::Error>(())
159/// ```
160pub fn run_with_verb(verb: &str, executable: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
161    let verb = normalize_shell_verb(verb)?;
162    let executable = executable.as_ref();
163
164    validate_executable(executable)?;
165    validate_command_args(args)?;
166
167    shell_execute_command(verb, executable, args, "ShellExecuteW")
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{
173        join_args_for_shell_execute, normalize_shell_verb, validate_command_args,
174        validate_executable, validate_restart_args,
175    };
176    use std::ffi::OsStr;
177    use std::ffi::OsString;
178
179    #[test]
180    fn join_args_empty_is_empty() {
181        let args: [OsString; 0] = [];
182        assert_eq!(join_args_for_shell_execute(&args), "");
183    }
184
185    #[test]
186    fn join_args_quotes_each_argument() {
187        let args = [OsString::from("alpha"), OsString::from("two words")];
188        assert_eq!(
189            join_args_for_shell_execute(&args),
190            "\"alpha\" \"two words\""
191        );
192    }
193
194    #[test]
195    fn join_args_escapes_inner_quotes() {
196        let args = [OsString::from("say \"hi\"")];
197        assert_eq!(join_args_for_shell_execute(&args), "\"say \\\"hi\\\"\"");
198    }
199
200    #[test]
201    fn join_args_doubles_trailing_backslashes_inside_quotes() {
202        let args = [OsString::from(r"C:\Program Files\demo\")];
203        assert_eq!(
204            join_args_for_shell_execute(&args),
205            r#""C:\Program Files\demo\\""#
206        );
207    }
208
209    #[test]
210    fn validate_restart_args_rejects_nul_bytes() {
211        let args = [OsString::from("hello\0world")];
212        let result = validate_restart_args(&args);
213        assert!(matches!(
214            result,
215            Err(crate::Error::InvalidInput(
216                "restart arguments cannot contain NUL bytes"
217            ))
218        ));
219    }
220
221    #[test]
222    fn validate_command_args_rejects_nul_bytes() {
223        let args = [OsString::from("hello\0world")];
224        let result = validate_command_args(&args);
225        assert!(matches!(
226            result,
227            Err(crate::Error::InvalidInput(
228                "arguments cannot contain NUL bytes"
229            ))
230        ));
231    }
232
233    #[test]
234    fn validate_executable_rejects_empty_string() {
235        let result = validate_executable(OsStr::new(""));
236        assert!(matches!(
237            result,
238            Err(crate::Error::InvalidInput("executable cannot be empty"))
239        ));
240    }
241
242    #[test]
243    fn validate_executable_rejects_nul_bytes() {
244        let result = validate_executable(OsStr::new("cmd\0.exe"));
245        assert!(matches!(
246            result,
247            Err(crate::Error::InvalidInput(
248                "executable cannot contain NUL bytes"
249            ))
250        ));
251    }
252
253    #[test]
254    fn normalize_shell_verb_rejects_empty_string() {
255        let result = normalize_shell_verb("");
256        assert!(matches!(
257            result,
258            Err(crate::Error::InvalidInput("verb cannot be empty"))
259        ));
260    }
261
262    #[test]
263    fn normalize_shell_verb_trims_surrounding_whitespace() {
264        assert_eq!(normalize_shell_verb("  runas  ").unwrap(), "runas");
265    }
266}