use std::io;
use windows::Win32::Foundation::{ERROR_FILE_NOT_FOUND, WIN32_ERROR};
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_QUERY_VALUE, KEY_SET_VALUE, REG_OPTION_NON_VOLATILE, REG_SZ,
RRF_RT_REG_SZ, RegCloseKey, RegCreateKeyExW, RegDeleteValueW, RegGetValueW, RegSetValueExW,
};
use windows::core::PCWSTR;
pub const MAX_STARTUP_NAME_UNITS: usize = 64;
pub const MAX_STARTUP_COMMAND_UNITS: usize = 2_048;
const RUN_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
pub fn set_startup_command(name: &str, command: &str) -> io::Result<()> {
let name = encode(name, MAX_STARTUP_NAME_UNITS, true)?;
let command = encode(command, MAX_STARTUP_COMMAND_UNITS, false)?;
let key = RunKey::open(KEY_SET_VALUE)?;
let bytes: Vec<u8> = command.iter().flat_map(|unit| unit.to_le_bytes()).collect();
check(unsafe { RegSetValueExW(key.0, PCWSTR(name.as_ptr()), None, REG_SZ, Some(&bytes)) })
}
pub fn remove_startup_command(name: &str) -> io::Result<bool> {
let name = encode(name, MAX_STARTUP_NAME_UNITS, true)?;
let key = RunKey::open(KEY_SET_VALUE)?;
let status = unsafe { RegDeleteValueW(key.0, PCWSTR(name.as_ptr())) };
if status == ERROR_FILE_NOT_FOUND {
return Ok(false);
}
check(status).map(|()| true)
}
pub fn startup_command(name: &str) -> io::Result<Option<String>> {
let name = encode(name, MAX_STARTUP_NAME_UNITS, true)?;
let key = RunKey::open(KEY_QUERY_VALUE)?;
let mut buffer = vec![0_u16; MAX_STARTUP_COMMAND_UNITS + 1];
let mut bytes = u32::try_from(buffer.len() * 2).unwrap_or(u32::MAX);
let status = unsafe {
RegGetValueW(
key.0,
PCWSTR::null(),
PCWSTR(name.as_ptr()),
RRF_RT_REG_SZ,
None,
Some(buffer.as_mut_ptr().cast()),
Some(&mut bytes),
)
};
if status == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
check(status)?;
let units = usize::try_from(bytes / 2).unwrap_or(0);
let value = &buffer[..units.min(buffer.len())];
let value = value.strip_suffix(&[0]).unwrap_or(value);
String::from_utf16(value)
.map(Some)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "startup command is not UTF-16"))
}
struct RunKey(HKEY);
impl RunKey {
fn open(access: windows::Win32::System::Registry::REG_SAM_FLAGS) -> io::Result<Self> {
let path = encode(RUN_KEY, RUN_KEY.len(), false)?;
let mut key = HKEY::default();
check(unsafe {
RegCreateKeyExW(
HKEY_CURRENT_USER,
PCWSTR(path.as_ptr()),
None,
PCWSTR::null(),
REG_OPTION_NON_VOLATILE,
access,
None,
&mut key,
None,
)
})?;
Ok(Self(key))
}
}
impl Drop for RunKey {
fn drop(&mut self) {
let _ = unsafe { RegCloseKey(self.0) };
}
}
fn encode(text: &str, limit: usize, name: bool) -> io::Result<Vec<u16>> {
let units = text.encode_utf16().count();
if units == 0
|| units > limit
|| text.chars().any(char::is_control)
|| (name && text.contains('\\'))
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"startup entry text is empty, too long or contains control characters",
));
}
Ok(text.encode_utf16().chain(std::iter::once(0)).collect())
}
fn check(status: WIN32_ERROR) -> io::Result<()> {
if status.is_ok() {
Ok(())
} else {
Err(io::Error::from_raw_os_error(status.0 as i32))
}
}
#[cfg(test)]
mod tests {
use super::{
MAX_STARTUP_NAME_UNITS, remove_startup_command, set_startup_command, startup_command,
};
#[test]
fn entries_are_validated() {
assert!(set_startup_command("", "x").is_err());
assert!(set_startup_command("a\\b", "x").is_err());
assert!(set_startup_command(&"n".repeat(MAX_STARTUP_NAME_UNITS + 1), "x").is_err());
assert!(set_startup_command("moirai-test", "line\nbreak").is_err());
assert!(startup_command("a\\b").is_err());
}
#[test]
fn entries_round_trip_and_are_removed() {
let name = format!("moirai-startup-test-{}", std::process::id());
let command = r#""C:\Program Files\Moirai Test\app.exe" "--minimized""#;
assert_eq!(startup_command(&name).expect("absent"), None);
set_startup_command(&name, command).expect("set");
assert_eq!(
startup_command(&name).expect("read").as_deref(),
Some(command)
);
assert!(remove_startup_command(&name).expect("remove"));
assert!(!remove_startup_command(&name).expect("second remove"));
assert_eq!(startup_command(&name).expect("removed"), None);
}
}