use std::ffi::{OsString, c_void};
use std::os::windows::ffi::OsStringExt;
use windows::Win32::Foundation::{
CloseHandle, ERROR_FILE_NOT_FOUND, ERROR_MORE_DATA, ERROR_SUCCESS, HANDLE, WIN32_ERROR,
};
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, REG_DWORD, REG_EXPAND_SZ, REG_SAM_FLAGS, REG_SZ,
REG_VALUE_TYPE, RegCloseKey, RegOpenKeyExW, RegQueryValueExW,
};
use windows::Win32::System::Threading::CreateEventW;
use windows::core::PCWSTR;
use crate::error::Error;
pub(crate) fn wide(text: &str) -> Vec<u16> {
text.encode_utf16().chain(std::iter::once(0)).collect()
}
pub(crate) fn win32_error(context: impl Into<String>, code: WIN32_ERROR) -> Error {
Error::io(
context,
std::io::Error::from_raw_os_error(i32::try_from(code.0).unwrap_or(-1)),
)
}
pub(crate) fn hresult_error(context: impl Into<String>, error: windows::core::Error) -> Error {
Error::io(context, std::io::Error::from_raw_os_error(error.code().0))
}
fn root_name(root: HKEY) -> &'static str {
if root == HKEY_LOCAL_MACHINE {
"HKLM"
} else if root == HKEY_CURRENT_USER {
"HKCU"
} else {
"HKEY"
}
}
#[derive(Debug)]
pub(super) struct RegKey {
key: HKEY,
root: &'static str,
path: &'static str,
}
unsafe impl Send for RegKey {}
impl RegKey {
pub(super) fn open(
root: HKEY,
path: &'static str,
access: REG_SAM_FLAGS,
context: &str,
) -> Result<Option<Self>, Error> {
let path_w = wide(path);
let mut key = HKEY::default();
let status =
unsafe { RegOpenKeyExW(root, PCWSTR(path_w.as_ptr()), None, access, &raw mut key) };
match status {
ERROR_SUCCESS => Ok(Some(Self {
key,
root: root_name(root),
path,
})),
ERROR_FILE_NOT_FOUND => Ok(None),
other => Err(win32_error(context.to_owned(), other)),
}
}
pub(super) fn raw(&self) -> HKEY {
self.key
}
fn value_context(&self, value: &str) -> String {
format!(
r"reading registry value {value} under {}\{}",
self.root, self.path
)
}
pub(super) fn string_value(&self, name: &str) -> Result<Option<String>, Error> {
let Some((kind, data)) = self.raw_value(name)? else {
return Ok(None);
};
if kind != REG_SZ && kind != REG_EXPAND_SZ {
return Ok(None);
}
let units: Vec<u16> = data
.as_chunks::<2>()
.0
.iter()
.map(|pair| u16::from_le_bytes(*pair))
.take_while(|unit| *unit != 0)
.collect();
Ok(Some(
OsString::from_wide(&units).to_string_lossy().into_owned(),
))
}
pub(super) fn dword_value(&self, name: &str) -> Result<Option<u32>, Error> {
let Some((kind, data)) = self.raw_value(name)? else {
return Ok(None);
};
if kind != REG_DWORD || data.len() < 4 {
return Ok(None);
}
Ok(Some(u32::from_le_bytes([
data[0], data[1], data[2], data[3],
])))
}
fn raw_value(&self, name: &str) -> Result<Option<(REG_VALUE_TYPE, Vec<u8>)>, Error> {
let name_w = wide(name);
for attempt in 0..MAX_REQUERY_ATTEMPTS {
let mut kind = REG_VALUE_TYPE::default();
let mut len: u32 = 0;
let status = unsafe {
RegQueryValueExW(
self.key,
PCWSTR(name_w.as_ptr()),
None,
Some(&raw mut kind),
None,
Some(&raw mut len),
)
};
match status {
ERROR_SUCCESS => {}
ERROR_FILE_NOT_FOUND => return Ok(None),
other => {
return Err(win32_error(self.value_context(name), other));
}
}
let mut data = vec![0u8; len as usize];
let status = unsafe {
RegQueryValueExW(
self.key,
PCWSTR(name_w.as_ptr()),
None,
Some(&raw mut kind),
Some(data.as_mut_ptr()),
Some(&raw mut len),
)
};
if classify_requery(status, attempt, MAX_REQUERY_ATTEMPTS) == Requery::Retry {
crate::trace::debug!(
name = name,
attempt = attempt,
"the registry value grew between sizing it and reading it; requerying"
);
continue;
}
return match status {
ERROR_SUCCESS => {
data.truncate(len as usize);
Ok(Some((kind, data)))
}
ERROR_FILE_NOT_FOUND => Ok(None),
other => Err(win32_error(self.value_context(name), other)),
};
}
unreachable!("the requery loop always returns before exhausting its attempt budget")
}
}
impl Drop for RegKey {
fn drop(&mut self) {
unsafe {
let _ = RegCloseKey(self.key);
}
}
}
const MAX_REQUERY_ATTEMPTS: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Requery {
Done,
Retry,
Fail,
}
fn classify_requery(status: WIN32_ERROR, attempt: u32, max_attempts: u32) -> Requery {
match status {
ERROR_MORE_DATA if attempt + 1 < max_attempts => Requery::Retry,
ERROR_MORE_DATA => Requery::Fail,
_ => Requery::Done,
}
}
#[derive(Debug)]
pub(crate) struct Event(HANDLE);
unsafe impl Send for Event {}
impl Event {
pub(crate) fn new(manual_reset: bool, context: &str) -> Result<Self, Error> {
let handle = unsafe { CreateEventW(None, manual_reset, false, PCWSTR::null()) }
.map_err(|e| hresult_error(context.to_owned(), e))?;
Ok(Self(handle))
}
pub(crate) fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for Event {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
pub(crate) unsafe fn wide_ptr_to_string(ptr: *const u16) -> Option<String> {
if ptr.is_null() {
return None;
}
let mut len = 0usize;
while unsafe { *ptr.add(len) } != 0 {
len += 1;
}
let units = unsafe { std::slice::from_raw_parts(ptr, len) };
Some(OsString::from_wide(units).to_string_lossy().into_owned())
}
#[derive(Debug, Clone, Copy)]
pub(super) struct SendPtr(pub(super) *mut c_void);
unsafe impl Send for SendPtr {}
impl SendPtr {
pub(super) fn handle(self) -> HANDLE {
HANDLE(self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use windows::Win32::System::Registry::KEY_NOTIFY;
#[test]
fn error_more_data_retries_while_attempts_remain() {
assert_eq!(
classify_requery(ERROR_MORE_DATA, 0, MAX_REQUERY_ATTEMPTS),
Requery::Retry
);
assert_eq!(
classify_requery(
ERROR_MORE_DATA,
MAX_REQUERY_ATTEMPTS - 2,
MAX_REQUERY_ATTEMPTS
),
Requery::Retry
);
}
#[test]
fn error_more_data_fails_on_the_last_attempt() {
assert_eq!(
classify_requery(
ERROR_MORE_DATA,
MAX_REQUERY_ATTEMPTS - 1,
MAX_REQUERY_ATTEMPTS
),
Requery::Fail
);
assert_eq!(classify_requery(ERROR_MORE_DATA, 0, 1), Requery::Fail);
}
#[test]
fn success_and_not_found_are_terminal_regardless_of_attempt() {
for attempt in 0..MAX_REQUERY_ATTEMPTS {
assert_eq!(
classify_requery(ERROR_SUCCESS, attempt, MAX_REQUERY_ATTEMPTS),
Requery::Done
);
assert_eq!(
classify_requery(ERROR_FILE_NOT_FOUND, attempt, MAX_REQUERY_ATTEMPTS),
Requery::Done
);
}
assert_eq!(
classify_requery(WIN32_ERROR(5), 0, MAX_REQUERY_ATTEMPTS),
Requery::Done
);
}
#[test]
fn a_value_read_failure_names_the_key_and_not_only_the_value() {
fn read_failure(root: HKEY) -> String {
let key = RegKey::open(root, "Software", KEY_NOTIFY, "opening a key for this test")
.expect("opening `Software` for notifications must succeed")
.expect("`Software` exists under both roots this backend opens");
key.string_value("ProxyServer")
.expect_err("a notify-only key cannot answer a value query")
.to_string()
}
let user = read_failure(HKEY_CURRENT_USER);
let machine = read_failure(HKEY_LOCAL_MACHINE);
assert_ne!(
user, machine,
"the same path under two roots must not fail with the same message"
);
assert!(user.contains("HKCU"), "{user}");
assert!(machine.contains("HKLM"), "{machine}");
}
}