use std::ffi::OsString;
use std::fmt;
use std::num::NonZeroU32;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr::NonNull;
use windows::Win32::Foundation::{CloseHandle, SetLastError, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_HANDLE, ERROR_INVALID_PARAMETER, ERROR_INVALID_SID, ERROR_INVALID_WINDOW_HANDLE, ERROR_PARTIAL_COPY, ERROR_SUCCESS, HANDLE, HWND, MAX_PATH, RECT};
use windows::core::{Error as WinErr, PWSTR};
use windows::Win32::Security::{GetSidSubAuthority, GetSidSubAuthorityCount, GetTokenInformation, IsValidSid, TokenIntegrityLevel, TOKEN_MANDATORY_LABEL, TOKEN_QUERY};
use windows::Win32::System::Threading::{OpenProcess, OpenProcessToken, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION};
use windows::Win32::UI::WindowsAndMessaging::{GetClassNameW, GetForegroundWindow, GetWindowRect, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindow, IsWindowVisible};
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct WindowSnapshot {
pub title: String,
pub class_name: String,
pub rect: WindowRect,
pub thread_id: WinThreadId,
pub process_id: WinProcessId,
pub is_minimized: bool,
pub is_foreground: bool,
pub is_visible: bool,
pub executable: PathBuf,
pub integrity_level: IntegrityLevel,
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
pub enum WindowSnapshotFromHandleError {
#[error("The handle passed in was invalid")]
InvalidHandle,
#[error("Windows API error: {0}")]
WinErr(#[from] WinErr),
}
impl WindowSnapshot {
pub fn from_hwnd(hwnd: HWND) -> Result<Self, WindowSnapshotFromHandleError> {
if !is_valid_window(hwnd) {
Err(WindowSnapshotFromHandleError::InvalidHandle)?
}
let (thread_id, process_id) = unsafe { get_window_thread_process_id(hwnd)? };
let process_handle = unsafe { open_process_handle_limited_query(process_id)? };
let snapshot = unsafe {
Self {
title: get_window_title(hwnd)?,
class_name: get_window_class_name(hwnd)?,
rect: get_window_rect(hwnd)?,
thread_id,
process_id,
is_minimized: is_window_minimized(hwnd),
is_foreground: is_window_foreground(hwnd),
is_visible: is_window_visible(hwnd),
executable: get_process_executable_path(process_handle)?,
integrity_level: get_process_integrity_level(process_handle)?,
}
};
unsafe { CloseHandle(process_handle).expect("process handle should be valid"); }
Ok(snapshot)
}
}
unsafe fn open_process_handle_limited_query(pid: WinProcessId) -> Result<HANDLE, WinErr> {
match unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) } {
Ok(handle) => Ok(handle),
Err(err) if err == WinErr::from(ERROR_INVALID_PARAMETER) => panic!("PID from get_window_thread_process_id should be valid"),
Err(err) if err == WinErr::from(ERROR_INVALID_HANDLE) => panic!("PID from get_window_thread_process_id should be valid"),
Err(err) => Err(err),
}
}
fn is_valid_window(hwnd: HWND) -> bool {
unsafe { IsWindow(Some(hwnd)).as_bool() }
}
unsafe fn get_window_title(hwnd: HWND) -> Result<String, WinErr> {
unsafe { SetLastError(ERROR_SUCCESS) };
let len = unsafe { GetWindowTextLengthW(hwnd) } ;
if len == 0 {
return match WinErr::from_win32() {
err if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err if err == ERROR_INVALID_WINDOW_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err if err == ERROR_SUCCESS.into() => Ok(String::new()),
err => Err(err),
};
}
let mut buffer = vec![0; (len + 1) as usize];
match unsafe { GetWindowTextW(hwnd, &mut buffer) } {
0 => match WinErr::from_win32() {
err if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err if err == ERROR_INVALID_WINDOW_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err => Err(err),
},
copied => Ok(String::from_utf16_lossy(&buffer[..copied as usize]))
}
}
unsafe fn get_window_class_name(hwnd: HWND) -> Result<String, WinErr> {
const CLASS_NAME_MAX_LEN: usize = 256;
let mut buffer = [0u16; CLASS_NAME_MAX_LEN];
match unsafe { GetClassNameW(hwnd, &mut buffer) } {
0 => match WinErr::from_win32() {
err if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err if err == ERROR_INVALID_WINDOW_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err => Err(err),
},
copied => Ok(String::from_utf16_lossy(&buffer[..copied as usize]))
}
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct WindowRect {
left: i32,
top: i32,
right: i32,
bottom: i32,
}
impl WindowRect {
pub fn new(left: i32, top: i32, right: i32, bottom: i32) -> Option<Self> {
if left <= right && top <= bottom {
Some(Self { left, top, right, bottom })
} else {
None
}
}
pub fn left(self) -> i32 {
self.left
}
pub fn top(self) -> i32 {
self.top
}
pub fn right(self) -> i32 {
self.right
}
pub fn bottom(self) -> i32 {
self.bottom
}
pub fn top_left(self) -> (i32, i32) {
(self.left, self.top)
}
pub fn size(self) -> (u32, u32) {
let width = self.right - self.left;
let height = self.bottom - self.top;
(width as _, height as _)
}
}
unsafe fn get_window_rect(hwnd: HWND) -> Result<WindowRect, WinErr> {
let mut rect = RECT::default();
match unsafe { GetWindowRect(hwnd, &mut rect) } {
Ok(()) => {},
Err(err) if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
Err(err) if err == ERROR_INVALID_WINDOW_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
Err(err) => Err(err)?,
}
let rect = WindowRect::new(rect.left, rect.top, rect.right, rect.bottom)
.expect("window dimensions should be non-negative");
Ok(rect)
}
pub type WinThreadId = NonZeroU32;
pub type WinProcessId = u32;
unsafe fn get_window_thread_process_id(hwnd: HWND) -> Result<(WinThreadId, WinProcessId), WinErr> {
let mut process_id = 0;
let thread_id = unsafe { GetWindowThreadProcessId(hwnd, Some(&mut process_id)) };
match WinThreadId::new(thread_id) {
Some(thread_id) => Ok((thread_id, process_id)),
None => match WinErr::from_win32() {
err if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err if err == ERROR_INVALID_WINDOW_HANDLE.into() => unreachable!("caller should ensure hwnd is valid"),
err => Err(err)?,
},
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct IntegrityLevel(pub u32);
impl IntegrityLevel {
pub const UNTRUSTED: Self = Self(0);
pub const LOW: Self = Self(0x1000);
pub const MEDIUM: Self = Self(0x2000);
pub const MEDIUM_PLUS: Self = Self(0x2100);
pub const HIGH: Self = Self(0x3000);
pub const SYSTEM: Self = Self(0x4000);
pub const PROTECTED: Self = Self(0x5000);
pub const SECURE: Self = Self(0x7000);
}
impl fmt::Debug for IntegrityLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let field = match *self {
Self::UNTRUSTED => format!("Untrusted (0x{:04x})", self.0),
Self::LOW => format!("Low (0x{:04x})", self.0),
Self::MEDIUM => format!("Medium (0x{:04x})", self.0),
Self::MEDIUM_PLUS => format!("Medium-plus (0x{:04x})", self.0),
Self::HIGH => format!("High (0x{:04x})", self.0),
Self::SYSTEM => format!("System (0x{:04x})", self.0),
Self::PROTECTED => format!("Protected (0x{:04x})", self.0),
Self::SECURE => format!("Secure (0x{:04x})", self.0),
Self(rid) => format!("0x{:04x}", rid),
};
f.debug_tuple(&field).finish()
}
}
unsafe fn get_process_integrity_level(process_handle: HANDLE) -> Result<IntegrityLevel, WinErr> {
let mut token_handle = HANDLE::default();
match unsafe { OpenProcessToken(process_handle, TOKEN_QUERY, &mut token_handle) } {
Err(err) if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller must ensure handle is valid"),
Err(err) if err == ERROR_INVALID_PARAMETER.into() => unreachable!("access flags are valid, output ptr is valid & writable"),
Err(err) => Err(err)?,
Ok(()) => {}
}
let mut ret_len = 0;
match unsafe { GetTokenInformation(token_handle, TokenIntegrityLevel, None, 0, &mut ret_len) } {
Err(err) if err == ERROR_INVALID_PARAMETER.into() => unreachable!("parameters should be valid"),
Err(err) if err == ERROR_INVALID_HANDLE.into() => unreachable!("token handle should be valid"),
Err(err) if err != ERROR_INSUFFICIENT_BUFFER.into() => {
unsafe { CloseHandle(token_handle).expect("handle should be valid"); }
return Err(err);
},
_ => {},
}
let mut buffer = vec![0u8; ret_len as _];
match unsafe { GetTokenInformation(token_handle, TokenIntegrityLevel, Some(buffer.as_mut_ptr() as _), ret_len, &mut ret_len) } {
Err(err) if err == ERROR_INVALID_PARAMETER.into() => unreachable!("parameters should be valid"),
Err(err) if err == ERROR_INVALID_HANDLE.into() => unreachable!("token handle should be valid"),
Err(err) if err == ERROR_INSUFFICIENT_BUFFER.into() => unreachable!("buffer should be correctly sized"),
Err(err) => {
unsafe { CloseHandle(token_handle).expect("handle should be valid"); }
return Err(err);
},
_ => {},
}
if buffer.len() < size_of::<TOKEN_MANDATORY_LABEL>() || buffer.len() < ret_len as _ {
Err(ERROR_INSUFFICIENT_BUFFER)?
}
let security_id = unsafe { *buffer.as_ptr().cast::<TOKEN_MANDATORY_LABEL>() }.Label.Sid;
if security_id.is_invalid() || unsafe { !IsValidSid(security_id).as_bool() } {
Err(ERROR_INVALID_SID)?
}
let count = unsafe { GetSidSubAuthorityCount(security_id) };
let count = unsafe { *NonNull::new(count).ok_or(ERROR_INVALID_SID)?.as_ptr() } as u32;
let rid = unsafe { GetSidSubAuthority(security_id, count - 1) };
let rid = unsafe { *NonNull::new(rid).ok_or(ERROR_INVALID_SID)?.as_ptr() };
unsafe { CloseHandle(token_handle).expect("handle should be valid"); }
Ok(IntegrityLevel(rid))
}
unsafe fn get_process_executable_path(process_handle: HANDLE) -> Result<PathBuf, WinErr> {
let mut buffer = [0u16; MAX_PATH as usize + 1];
let mut chars = buffer.len() as _;
match unsafe { QueryFullProcessImageNameW(process_handle, PROCESS_NAME_WIN32, PWSTR(buffer.as_mut_ptr()), &mut chars) } {
Err(err) if err == ERROR_INSUFFICIENT_BUFFER.into() => unreachable!("paths should not be longer than MAX_PATH"),
Err(err) if err == ERROR_PARTIAL_COPY.into() => unreachable!("paths should not be longer than MAX_PATH"),
Err(err) if err == ERROR_INVALID_HANDLE.into() => unreachable!("caller should ensure process_handle is valid"),
Err(err) if err == ERROR_INVALID_PARAMETER.into() => unreachable!("params should be valid"),
Err(err) => Err(err)?,
Ok(()) => {}
}
assert!(
chars < (buffer.len() - 1) as u32,
"QueryFullProcessImageNameW should not return more characters than the buffer can hold",
);
Ok(PathBuf::from(OsString::from_wide(&buffer[..chars as _])))
}
unsafe fn is_window_minimized(hwnd: HWND) -> bool {
unsafe { IsIconic(hwnd).as_bool() }
}
unsafe fn is_window_visible(hwnd: HWND) -> bool {
unsafe { IsWindowVisible(hwnd).as_bool() }
}
unsafe fn is_window_foreground(hwnd: HWND) -> bool {
hwnd == unsafe { GetForegroundWindow() }
}