use winreg::RegKey;
use winreg::enums::*;
use crate::error::{Error, Result};
use crate::protocol::Protocol;
const CLASSES_PATH: &str = r"Software\Classes";
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PATH_NOT_FOUND: i32 = 3;
const ERROR_ACCESS_DENIED: i32 = 5;
pub(crate) fn is_elevated() -> bool {
is_admin::is_admin()
}
pub(crate) fn ensure_elevated() -> Result<()> {
if is_elevated() {
Ok(())
} else {
Err(Error::ElevationRequired(
"the current process is not running with administrator privileges".to_string(),
))
}
}
fn hklm_classes() -> Result<RegKey> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
match hklm.open_subkey_with_flags(CLASSES_PATH, KEY_READ | KEY_WRITE) {
Ok(key) => Ok(key),
Err(err) if err.raw_os_error() == Some(ERROR_ACCESS_DENIED) => {
Err(Error::ElevationRequired(err.to_string()))
}
Err(err) => Err(err.into()),
}
}
fn open_optional(root: &RegKey, path: &str) -> Result<Option<RegKey>> {
match root.open_subkey_with_flags(path, KEY_READ) {
Ok(key) => Ok(Some(key)),
Err(err)
if matches!(
err.raw_os_error(),
Some(ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND)
) =>
{
Ok(None)
}
Err(err) => Err(err.into()),
}
}
pub(crate) fn read_protocol(name: &str) -> Result<Option<Protocol>> {
let hkcr = RegKey::predef(HKEY_CLASSES_ROOT);
let Some(key) = open_optional(&hkcr, name)? else {
return Ok(None);
};
Ok(Some(protocol_from_key(&key, name)))
}
fn protocol_from_key(key: &RegKey, name: &str) -> Protocol {
let description = key.get_value("").unwrap_or_default();
let url_protocol = key.get_value::<String, _>("URL Protocol").is_ok();
let icon = key
.open_subkey("DefaultIcon")
.ok()
.and_then(|icon_key| icon_key.get_value("").ok());
let command = key
.open_subkey(r"shell\open\command")
.ok()
.and_then(|command_key| command_key.get_value("").ok())
.unwrap_or_default();
Protocol {
name: name.to_string(),
description,
url_protocol,
icon,
command,
}
}
pub(crate) fn write_protocol(protocol: &Protocol) -> Result<()> {
ensure_elevated()?;
let classes = hklm_classes()?;
let (root, _disposition) = classes.create_subkey(&protocol.name)?;
root.set_value("", &protocol.description)?;
if protocol.url_protocol {
root.set_value("URL Protocol", &"")?;
} else {
let _ = root.delete_value("URL Protocol");
}
match &protocol.icon {
Some(icon) => {
let (icon_key, _) = root.create_subkey("DefaultIcon")?;
icon_key.set_value("", icon)?;
}
None => {
if root.open_subkey("DefaultIcon").is_ok() {
let _ = root.delete_subkey_all("DefaultIcon");
}
}
}
let (shell, _) = root.create_subkey(r"shell\open\command")?;
shell.set_value("", &protocol.command)?;
Ok(())
}
pub(crate) fn delete_protocol(name: &str) -> Result<bool> {
let mut removed = false;
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let in_hklm = hklm
.open_subkey(CLASSES_PATH)
.map(|classes| classes.open_subkey(name).is_ok())
.unwrap_or(false);
if in_hklm {
ensure_elevated()?;
hklm_classes()?.delete_subkey_all(name)?;
removed = true;
}
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
if let Ok(classes) = hkcu.open_subkey_with_flags(CLASSES_PATH, KEY_READ | KEY_WRITE)
&& classes.open_subkey(name).is_ok()
{
classes.delete_subkey_all(name)?;
removed = true;
}
Ok(removed)
}
pub(crate) fn list_url_protocols() -> Result<Vec<Protocol>> {
let hkcr = RegKey::predef(HKEY_CLASSES_ROOT);
let mut protocols = Vec::new();
for entry in hkcr.enum_keys() {
let name = entry?;
if !Protocol::is_valid_name(&name) {
continue;
}
let Some(key) = open_optional(&hkcr, &name)? else {
continue;
};
if key.get_value::<String, _>("URL Protocol").is_err() {
continue;
}
protocols.push(protocol_from_key(&key, &name));
}
Ok(protocols)
}
const BROWSER_POLICY_KEYS: &[&str] = &[
r"Software\Policies\Google\Chrome",
r"Software\Policies\Microsoft\Edge",
];
const ALWAYS_OPEN_CHECKBOX_VALUE: &str = "ExternalProtocolDialogShowAlwaysOpenCheckbox";
pub(crate) fn set_browser_policy(enabled: bool) -> Result<()> {
ensure_elevated()?;
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
for path in BROWSER_POLICY_KEYS {
let (key, _) = hklm.create_subkey(path)?;
if enabled {
key.set_value(ALWAYS_OPEN_CHECKBOX_VALUE, &1u32)?;
} else {
match key.delete_value(ALWAYS_OPEN_CHECKBOX_VALUE) {
Err(err) if err.raw_os_error() == Some(ERROR_FILE_NOT_FOUND) => {}
other => other?,
}
}
}
Ok(())
}