Skip to main content

pacenotes_installer/
elevation.rs

1//! Restarting as administrator, for a game folder this account cannot write
2//! (P11): what the new instance carries over so nothing is asked twice, and
3//! the UAC launch itself.
4
5const RESTARTED: &str = "--restarted";
6const LANGUAGE: &str = "--language";
7const FOLDER: &str = "--folder";
8const SECRET: &str = "--secret";
9/// Developer overrides a restart must keep, or it would talk to another server.
10const PASSED_ON: [&str; 2] = ["--server", "--cache"];
11
12/// What a restarted instance resumes from; it opens on scene 3.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Restart {
15    pub language: crate::i18n::Language,
16    pub folder: String,
17    /// On the elevated process's command line, readable by other programs
18    /// running as this user: accepted by the maintainer for a seamless restart.
19    pub secret: String,
20}
21
22impl Restart {
23    /// This process's own restart, when it is one.
24    pub fn from_args() -> Option<Restart> {
25        let arguments: Vec<String> = std::env::args().skip(1).collect();
26        Restart::parse(&arguments)
27    }
28
29    fn parse(arguments: &[String]) -> Option<Restart> {
30        if !arguments.iter().any(|argument| argument == RESTARTED) {
31            return None;
32        }
33        let value = |flag: &str| {
34            let at = arguments.iter().position(|argument| argument == flag)?;
35            arguments.get(at + 1).cloned()
36        };
37        Some(Restart {
38            language: crate::i18n::Language::from_tag(&value(LANGUAGE)?)?,
39            folder: value(FOLDER)?,
40            secret: value(SECRET).unwrap_or_default(),
41        })
42    }
43
44    /// The command line of the restarted instance, with the developer
45    /// overrides this one was given.
46    fn command_line(&self, passed_on: &[(&str, String)]) -> String {
47        let mut arguments = vec![
48            RESTARTED.to_string(),
49            LANGUAGE.to_string(),
50            self.language.tag().to_string(),
51            FOLDER.to_string(),
52            self.folder.clone(),
53            SECRET.to_string(),
54            self.secret.clone(),
55        ];
56        for (flag, value) in passed_on {
57            arguments.push(flag.to_string());
58            arguments.push(value.clone());
59        }
60        arguments
61            .iter()
62            .map(|argument| quote(argument))
63            .collect::<Vec<_>>()
64            .join(" ")
65    }
66}
67
68/// Whether this process already runs elevated, when offering elevation again
69/// would only loop.
70pub fn is_elevated() -> bool {
71    unsafe {
72        let mut token: windows_sys::Win32::Foundation::HANDLE = core::ptr::null_mut();
73        if windows_sys::Win32::System::Threading::OpenProcessToken(
74            windows_sys::Win32::System::Threading::GetCurrentProcess(),
75            windows_sys::Win32::Security::TOKEN_QUERY,
76            &mut token,
77        ) == 0
78        {
79            return false;
80        }
81        let mut elevation = windows_sys::Win32::Security::TOKEN_ELEVATION { TokenIsElevated: 0 };
82        let mut written = 0u32;
83        let read = windows_sys::Win32::Security::GetTokenInformation(
84            token,
85            windows_sys::Win32::Security::TokenElevation,
86            &mut elevation as *mut _ as *mut core::ffi::c_void,
87            core::mem::size_of::<windows_sys::Win32::Security::TOKEN_ELEVATION>() as u32,
88            &mut written,
89        );
90        windows_sys::Win32::Foundation::CloseHandle(token);
91        read != 0 && elevation.TokenIsElevated != 0
92    }
93}
94
95/// Starts this executable again through UAC; false when the user cancels the
96/// prompt or it cannot start, which leaves this instance where it was.
97pub fn restart_elevated(owner: windows_sys::Win32::Foundation::HWND, restart: &Restart) -> bool {
98    let Ok(exe) = std::env::current_exe() else {
99        return false;
100    };
101    let passed_on: Vec<(&str, String)> = PASSED_ON
102        .iter()
103        .filter_map(|flag| Some((*flag, crate::argument_after(flag)?)))
104        .collect();
105    let verb = crate::i18n::to_wide("runas");
106    let file = crate::i18n::to_wide(&exe.to_string_lossy());
107    let parameters = crate::i18n::to_wide(&restart.command_line(&passed_on));
108    let started = unsafe {
109        windows_sys::Win32::UI::Shell::ShellExecuteW(
110            owner,
111            verb.as_ptr(),
112            file.as_ptr(),
113            parameters.as_ptr(),
114            core::ptr::null(),
115            windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
116        )
117    };
118    // Anything above 32 is success, by ShellExecute's own convention.
119    started as isize > 32
120}
121
122/// One argument as `CommandLineToArgvW` and Rust's `std::env::args` split it
123/// back: quoted, with the backslashes before a quote doubled.
124fn quote(argument: &str) -> String {
125    if !argument.is_empty() && !argument.contains([' ', '\t', '"']) {
126        return argument.to_string();
127    }
128    let mut quoted = String::from('"');
129    // Backslashes count only before a quote, including the closing one.
130    let mut backslashes = 0;
131    for character in argument.chars() {
132        if character == '"' {
133            quoted.push_str(&"\\".repeat(backslashes + 1));
134        }
135        backslashes = if character == '\\' {
136            backslashes + 1
137        } else {
138            0
139        };
140        quoted.push(character);
141    }
142    quoted.push_str(&"\\".repeat(backslashes));
143    quoted.push('"');
144    quoted
145}
146
147#[cfg(test)]
148mod tests;