feat(client): add TAP-Windows adapter crate
The Windows client still needs a real Ethernet TAP boundary before it can pump
frames between the adapter and the relay session. Keep that OS-specific surface
separate from the QUIC client state by adding a `lanparty-client-tap` crate.
The new crate discovers TAP-Windows6 adapters through the Windows network
adapter registry class, validates `tap0901` component ids, constructs the
`\\.\Global\{NetCfgInstanceId}.tap` device path, opens the TAP device handle,
and exposes blocking Ethernet frame read/write helpers. It also wraps the
TAP-Windows IOCTLs for media status, driver MAC, and driver MTU.
This does not wire the TAP crate into `lanparty-client-win` yet and does not
attempt route protection. The value of this slice is the target-checkable OS
boundary that the next client pump can depend on.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- Windows-target cargo check for lanparty-client-tap with clang-cl/lld-link
- Windows-target cargo clippy for lanparty-client-tap with -D warnings
- git diff --check
Refs: PLAN.md Windows TAP client
This commit is contained in:
Generated
+9
@@ -446,6 +446,15 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lanparty-client-tap"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"lanparty-proto",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lanparty-client-win"
|
name = "lanparty-client-win"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"crates/lanparty-client-core",
|
"crates/lanparty-client-core",
|
||||||
|
"crates/lanparty-client-tap",
|
||||||
"crates/lanparty-client-win",
|
"crates/lanparty-client-win",
|
||||||
"crates/lanparty-ctrl",
|
"crates/lanparty-ctrl",
|
||||||
"crates/lanparty-gateway",
|
"crates/lanparty-gateway",
|
||||||
@@ -28,3 +29,4 @@ serde_json = "1"
|
|||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
windows-sys = "0.61.2"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Monorepo for a Layer 2 over QUIC LAN party bridge.
|
|||||||
- `lanparty-ctrl`: control-plane messages (join/hello/role/version).
|
- `lanparty-ctrl`: control-plane messages (join/hello/role/version).
|
||||||
- `lanparty-obs`: shared diagnostics/logging event models.
|
- `lanparty-obs`: shared diagnostics/logging event models.
|
||||||
- `lanparty-client-core`: platform-agnostic client session state.
|
- `lanparty-client-core`: platform-agnostic client session state.
|
||||||
|
- `lanparty-client-tap`: TAP-Windows6 adapter discovery and frame I/O.
|
||||||
- `lanparty-client-win`: Windows TAP + route/metric handling binary.
|
- `lanparty-client-win`: Windows TAP + route/metric handling binary.
|
||||||
- `lanparty-gateway`: Linux AF_PACKET gateway binary.
|
- `lanparty-gateway`: Linux AF_PACKET gateway binary.
|
||||||
- `lanparty-relay`: public QUIC relay binary.
|
- `lanparty-relay`: public QUIC relay binary.
|
||||||
@@ -47,6 +48,15 @@ Platform-neutral remote client relay session:
|
|||||||
- welcome/reject handling with assigned peer id and effective TAP MTU
|
- welcome/reject handling with assigned peer id and effective TAP MTU
|
||||||
- Ethernet frame send/receive helpers over QUIC DATAGRAM
|
- Ethernet frame send/receive helpers over QUIC DATAGRAM
|
||||||
|
|
||||||
|
### `lanparty-client-tap`
|
||||||
|
|
||||||
|
Windows TAP adapter boundary:
|
||||||
|
|
||||||
|
- TAP-Windows6 adapter discovery from the Windows network adapter registry
|
||||||
|
- `\\.\Global\{NetCfgInstanceId}.tap` device path construction
|
||||||
|
- blocking Ethernet frame reads/writes through the TAP device handle
|
||||||
|
- TAP driver IOCTL helpers for media status, adapter MAC, and MTU
|
||||||
|
|
||||||
### `lanparty-relay`
|
### `lanparty-relay`
|
||||||
|
|
||||||
Public relay binary and relay-owned room state:
|
Public relay binary and relay-owned room state:
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "lanparty-client-tap"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
lanparty-proto = { path = "../lanparty-proto" }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-sys = { workspace = true, features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
"Win32_System_IO",
|
||||||
|
"Win32_System_Registry",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! TAP-Windows6 adapter discovery and raw Ethernet frame I/O.
|
||||||
|
//!
|
||||||
|
//! This crate deliberately stays below the relay session layer. It only knows
|
||||||
|
//! how to find and open an installed TAP-Windows6 Ethernet adapter; the Windows
|
||||||
|
//! client binary owns when to connect it to QUIC and how to protect routes.
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
|
pub const TAP_COMPONENT_ID: &str = "tap0901";
|
||||||
|
pub const TAP_ADAPTER_KEY: &str =
|
||||||
|
r"SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}";
|
||||||
|
pub const TAP_DEVICE_PREFIX: &str = r"\\.\Global\";
|
||||||
|
pub const TAP_DEVICE_SUFFIX: &str = ".tap";
|
||||||
|
const FILE_DEVICE_UNKNOWN: u32 = 0x0000_0022;
|
||||||
|
const METHOD_BUFFERED: u32 = 0;
|
||||||
|
const FILE_ANY_ACCESS: u32 = 0;
|
||||||
|
const TAP_IOCTL_GET_MAC_REQUEST: u32 = 1;
|
||||||
|
const TAP_IOCTL_GET_MTU_REQUEST: u32 = 3;
|
||||||
|
const TAP_IOCTL_SET_MEDIA_STATUS_REQUEST: u32 = 6;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TapAdapterInfo {
|
||||||
|
instance_id: String,
|
||||||
|
component_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TapAdapterInfo {
|
||||||
|
pub fn new(instance_id: impl Into<String>, component_id: impl Into<String>) -> Result<Self> {
|
||||||
|
let instance_id = instance_id.into();
|
||||||
|
if instance_id.trim().is_empty() {
|
||||||
|
bail!("TAP adapter instance id cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let component_id = component_id.into();
|
||||||
|
if !is_tap_component_id(&component_id) {
|
||||||
|
bail!("unsupported TAP adapter component id {component_id:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
instance_id,
|
||||||
|
component_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn instance_id(&self) -> &str {
|
||||||
|
&self.instance_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn component_id(&self) -> &str {
|
||||||
|
&self.component_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn device_path(&self) -> String {
|
||||||
|
tap_device_path(&self.instance_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_tap_component_id(component_id: &str) -> bool {
|
||||||
|
let id = component_id.trim();
|
||||||
|
id.eq_ignore_ascii_case(TAP_COMPONENT_ID)
|
||||||
|
|| id.eq_ignore_ascii_case(concat!("root\\", "tap0901"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn tap_device_path(instance_id: &str) -> String {
|
||||||
|
format!("{TAP_DEVICE_PREFIX}{instance_id}{TAP_DEVICE_SUFFIX}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tap_control_code(request: u32) -> u32 {
|
||||||
|
(FILE_DEVICE_UNKNOWN << 16) | (FILE_ANY_ACCESS << 14) | (request << 2) | METHOD_BUFFERED
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tap_ioctl_get_mac() -> u32 {
|
||||||
|
tap_control_code(TAP_IOCTL_GET_MAC_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tap_ioctl_get_mtu() -> u32 {
|
||||||
|
tap_control_code(TAP_IOCTL_GET_MTU_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tap_ioctl_set_media_status() -> u32 {
|
||||||
|
tap_control_code(TAP_IOCTL_SET_MEDIA_STATUS_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use windows::{TapAdapter, available_adapters, open_first_adapter};
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
||||||
|
bail!("TAP-Windows6 adapter discovery is only available on Windows");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identifies_tap_windows_component_ids() {
|
||||||
|
assert!(is_tap_component_id("tap0901"));
|
||||||
|
assert!(is_tap_component_id("TAP0901"));
|
||||||
|
assert!(is_tap_component_id("root\\tap0901"));
|
||||||
|
assert!(!is_tap_component_id("wintun"));
|
||||||
|
assert!(!is_tap_component_id("tap0801"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_tap_device_path() {
|
||||||
|
assert_eq!(
|
||||||
|
tap_device_path("{01234567-89AB-CDEF-0123-456789ABCDEF}"),
|
||||||
|
r"\\.\Global\{01234567-89AB-CDEF-0123-456789ABCDEF}.tap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn computes_tap_ioctl_codes() {
|
||||||
|
assert_eq!(tap_ioctl_get_mac(), 0x0022_0004);
|
||||||
|
assert_eq!(tap_ioctl_get_mtu(), 0x0022_000c);
|
||||||
|
assert_eq!(tap_ioctl_set_media_status(), 0x0022_0018);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_adapter_info() {
|
||||||
|
let info =
|
||||||
|
TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "tap0901").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(info.component_id(), "tap0901");
|
||||||
|
assert_eq!(
|
||||||
|
info.device_path(),
|
||||||
|
r"\\.\Global\{01234567-89AB-CDEF-0123-456789ABCDEF}.tap"
|
||||||
|
);
|
||||||
|
assert!(TapAdapterInfo::new("", "tap0901").is_err());
|
||||||
|
assert!(TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "wintun").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::c_void,
|
||||||
|
io::{self, ErrorKind},
|
||||||
|
ptr::{null, null_mut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use lanparty_proto::MacAddr;
|
||||||
|
use windows_sys::Win32::{
|
||||||
|
Foundation::{
|
||||||
|
CloseHandle, ERROR_FILE_NOT_FOUND, ERROR_MORE_DATA, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS,
|
||||||
|
GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
|
||||||
|
},
|
||||||
|
Storage::FileSystem::{
|
||||||
|
CreateFileW, FILE_ATTRIBUTE_SYSTEM, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
||||||
|
ReadFile, WriteFile,
|
||||||
|
},
|
||||||
|
System::{
|
||||||
|
IO::DeviceIoControl,
|
||||||
|
Registry::{
|
||||||
|
HKEY, HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ, RegCloseKey, RegEnumKeyExW, RegOpenKeyExW,
|
||||||
|
RegQueryValueExW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
TAP_ADAPTER_KEY, TapAdapterInfo, is_tap_component_id, tap_ioctl_get_mac, tap_ioctl_get_mtu,
|
||||||
|
tap_ioctl_set_media_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TapAdapter {
|
||||||
|
info: TapAdapterInfo,
|
||||||
|
handle: OwnedHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TapAdapter {
|
||||||
|
pub fn open(info: TapAdapterInfo) -> Result<Self> {
|
||||||
|
let path = info.device_path();
|
||||||
|
let wide_path = wide_null(&path);
|
||||||
|
let handle = unsafe {
|
||||||
|
// SAFETY: wide_path is NUL-terminated and lives for the duration of the call.
|
||||||
|
// The security attributes and template handle are intentionally null.
|
||||||
|
CreateFileW(
|
||||||
|
wide_path.as_ptr(),
|
||||||
|
GENERIC_READ | GENERIC_WRITE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
null(),
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_ATTRIBUTE_SYSTEM,
|
||||||
|
null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let handle = OwnedHandle::new(handle)
|
||||||
|
.with_context(|| format!("failed to open TAP adapter device {path}"))?;
|
||||||
|
|
||||||
|
Ok(Self { info, handle })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn info(&self) -> &TapAdapterInfo {
|
||||||
|
&self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_media_connected(&self, connected: bool) -> Result<()> {
|
||||||
|
let mut status = u32::from(connected);
|
||||||
|
self.device_io_control(
|
||||||
|
tap_ioctl_set_media_status(),
|
||||||
|
(&mut status as *mut u32).cast::<c_void>(),
|
||||||
|
std::mem::size_of::<u32>() as u32,
|
||||||
|
null_mut(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.context("failed to set TAP media status")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn driver_mac(&self) -> Result<MacAddr> {
|
||||||
|
let mut bytes = [0_u8; 6];
|
||||||
|
self.device_io_control(
|
||||||
|
tap_ioctl_get_mac(),
|
||||||
|
null_mut(),
|
||||||
|
0,
|
||||||
|
bytes.as_mut_ptr().cast::<c_void>(),
|
||||||
|
bytes.len() as u32,
|
||||||
|
)
|
||||||
|
.context("failed to read TAP driver MAC address")?;
|
||||||
|
|
||||||
|
Ok(MacAddr::new(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn driver_mtu(&self) -> Result<u32> {
|
||||||
|
let mut mtu = 0_u32;
|
||||||
|
self.device_io_control(
|
||||||
|
tap_ioctl_get_mtu(),
|
||||||
|
null_mut(),
|
||||||
|
0,
|
||||||
|
(&mut mtu as *mut u32).cast::<c_void>(),
|
||||||
|
std::mem::size_of::<u32>() as u32,
|
||||||
|
)
|
||||||
|
.context("failed to read TAP driver MTU")?;
|
||||||
|
|
||||||
|
Ok(mtu)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_frame(&self, buffer: &mut [u8]) -> Result<usize> {
|
||||||
|
let mut bytes_read = 0_u32;
|
||||||
|
let ok = unsafe {
|
||||||
|
// SAFETY: buffer is valid for writes of buffer.len() bytes and the handle is owned by
|
||||||
|
// this adapter. The synchronous handle uses a null OVERLAPPED pointer.
|
||||||
|
ReadFile(
|
||||||
|
self.handle.raw(),
|
||||||
|
buffer.as_mut_ptr(),
|
||||||
|
buffer
|
||||||
|
.len()
|
||||||
|
.try_into()
|
||||||
|
.context("TAP read buffer is too large")?,
|
||||||
|
&mut bytes_read,
|
||||||
|
null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(io::Error::last_os_error()).context("failed to read TAP frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bytes_read as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_frame(&self, frame: &[u8]) -> Result<usize> {
|
||||||
|
let mut bytes_written = 0_u32;
|
||||||
|
let ok = unsafe {
|
||||||
|
// SAFETY: frame is valid for reads of frame.len() bytes and the handle is owned by
|
||||||
|
// this adapter. The synchronous handle uses a null OVERLAPPED pointer.
|
||||||
|
WriteFile(
|
||||||
|
self.handle.raw(),
|
||||||
|
frame.as_ptr(),
|
||||||
|
frame.len().try_into().context("TAP frame is too large")?,
|
||||||
|
&mut bytes_written,
|
||||||
|
null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(io::Error::last_os_error()).context("failed to write TAP frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bytes_written as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_io_control(
|
||||||
|
&self,
|
||||||
|
code: u32,
|
||||||
|
input: *mut c_void,
|
||||||
|
input_len: u32,
|
||||||
|
output: *mut c_void,
|
||||||
|
output_len: u32,
|
||||||
|
) -> io::Result<u32> {
|
||||||
|
let mut bytes_returned = 0_u32;
|
||||||
|
let ok = unsafe {
|
||||||
|
// SAFETY: input/output pointers and lengths are supplied by the typed public methods
|
||||||
|
// above. The synchronous handle uses a null OVERLAPPED pointer.
|
||||||
|
DeviceIoControl(
|
||||||
|
self.handle.raw(),
|
||||||
|
code,
|
||||||
|
input.cast_const(),
|
||||||
|
input_len,
|
||||||
|
output,
|
||||||
|
output_len,
|
||||||
|
&mut bytes_returned,
|
||||||
|
null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bytes_returned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_first_adapter() -> Result<TapAdapter> {
|
||||||
|
let mut adapters = available_adapters()?;
|
||||||
|
let info = adapters
|
||||||
|
.drain(..)
|
||||||
|
.next()
|
||||||
|
.context("no TAP-Windows6 adapters found")?;
|
||||||
|
|
||||||
|
TapAdapter::open(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
||||||
|
let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY)
|
||||||
|
.context("failed to open TAP adapter registry key")?;
|
||||||
|
let mut adapters = Vec::new();
|
||||||
|
|
||||||
|
for subkey in adapters_key.subkey_names()? {
|
||||||
|
let subkey = adapters_key
|
||||||
|
.open_subkey(&subkey)
|
||||||
|
.with_context(|| format!("failed to open TAP adapter registry subkey {subkey}"))?;
|
||||||
|
let Some(component_id) = subkey.query_string("ComponentId")? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !is_tap_component_id(&component_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(instance_id) = subkey.query_string("NetCfgInstanceId")? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
adapters.push(TapAdapterInfo::new(instance_id, component_id)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(adapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OwnedHandle(HANDLE);
|
||||||
|
|
||||||
|
impl OwnedHandle {
|
||||||
|
fn new(handle: HANDLE) -> io::Result<Self> {
|
||||||
|
if handle == INVALID_HANDLE_VALUE {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(Self(handle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn raw(&self) -> HANDLE {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for OwnedHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
// SAFETY: self.0 is a valid owned HANDLE created by CreateFileW.
|
||||||
|
CloseHandle(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RegKey(HKEY);
|
||||||
|
|
||||||
|
impl RegKey {
|
||||||
|
fn open(root: HKEY, path: &str) -> io::Result<Self> {
|
||||||
|
let path = wide_null(path);
|
||||||
|
let mut key = null_mut();
|
||||||
|
let status = unsafe {
|
||||||
|
// SAFETY: path is NUL-terminated and phkresult points to valid storage.
|
||||||
|
RegOpenKeyExW(root, path.as_ptr(), 0, KEY_READ, &mut key)
|
||||||
|
};
|
||||||
|
windows_status(status)?;
|
||||||
|
|
||||||
|
Ok(Self(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_subkey(&self, path: &str) -> io::Result<Self> {
|
||||||
|
Self::open(self.0, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subkey_names(&self) -> io::Result<Vec<String>> {
|
||||||
|
let mut names = Vec::new();
|
||||||
|
let mut index = 0_u32;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut buffer = vec![0_u16; 256];
|
||||||
|
let mut len = buffer.len() as u32;
|
||||||
|
let status = unsafe {
|
||||||
|
// SAFETY: buffer is valid for len UTF-16 code units and len points to storage
|
||||||
|
// that RegEnumKeyExW updates with the returned name length.
|
||||||
|
RegEnumKeyExW(
|
||||||
|
self.0,
|
||||||
|
index,
|
||||||
|
buffer.as_mut_ptr(),
|
||||||
|
&mut len,
|
||||||
|
null(),
|
||||||
|
null_mut(),
|
||||||
|
null_mut(),
|
||||||
|
null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
match status {
|
||||||
|
ERROR_SUCCESS => {
|
||||||
|
buffer.truncate(len as usize);
|
||||||
|
names.push(String::from_utf16_lossy(&buffer));
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
ERROR_NO_MORE_ITEMS => return Ok(names),
|
||||||
|
ERROR_MORE_DATA => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
"registry subkey name exceeded internal buffer",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
status => return Err(windows_error(status)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_string(&self, name: &str) -> io::Result<Option<String>> {
|
||||||
|
let name = wide_null(name);
|
||||||
|
let mut value_type = 0_u32;
|
||||||
|
let mut byte_len = 0_u32;
|
||||||
|
let status = unsafe {
|
||||||
|
// SAFETY: name is NUL-terminated. A null data buffer asks Windows for the byte size.
|
||||||
|
RegQueryValueExW(
|
||||||
|
self.0,
|
||||||
|
name.as_ptr(),
|
||||||
|
null(),
|
||||||
|
&mut value_type,
|
||||||
|
null_mut(),
|
||||||
|
&mut byte_len,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
match status {
|
||||||
|
ERROR_SUCCESS => {}
|
||||||
|
ERROR_FILE_NOT_FOUND => return Ok(None),
|
||||||
|
status => return Err(windows_error(status)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if value_type != REG_SZ {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if byte_len == 0 {
|
||||||
|
return Ok(Some(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = vec![0_u16; byte_len.div_ceil(2) as usize];
|
||||||
|
let status = unsafe {
|
||||||
|
// SAFETY: buffer is valid for byte_len bytes and name remains NUL-terminated.
|
||||||
|
RegQueryValueExW(
|
||||||
|
self.0,
|
||||||
|
name.as_ptr(),
|
||||||
|
null(),
|
||||||
|
&mut value_type,
|
||||||
|
buffer.as_mut_ptr().cast::<u8>(),
|
||||||
|
&mut byte_len,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
windows_status(status)?;
|
||||||
|
let nul = buffer
|
||||||
|
.iter()
|
||||||
|
.position(|value| *value == 0)
|
||||||
|
.unwrap_or(buffer.len());
|
||||||
|
buffer.truncate(nul);
|
||||||
|
|
||||||
|
Ok(Some(String::from_utf16_lossy(&buffer)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RegKey {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
// SAFETY: self.0 is a valid open registry key owned by this value.
|
||||||
|
RegCloseKey(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows_status(status: u32) -> io::Result<()> {
|
||||||
|
if status == ERROR_SUCCESS {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(windows_error(status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows_error(status: u32) -> io::Error {
|
||||||
|
io::Error::from_raw_os_error(status as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wide_null(value: &str) -> Vec<u16> {
|
||||||
|
value.encode_utf16().chain(std::iter::once(0)).collect()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user