#[cfg(target_os = "windows")]
use std::io::{BufRead, Write};
use std::process::ExitCode;
#[cfg(not(target_os = "windows"))]
fn main() -> ExitCode {
eprintln!(
"crcbl-e2e-win32-input: SendInput is a Win32 call; there is no desktop here to type at"
);
ExitCode::FAILURE
}
#[cfg(target_os = "windows")]
mod win32 {
use core::ffi::c_void;
pub type Handle = *mut c_void;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct MouseInput {
pub dx: i32,
pub dy: i32,
pub mouse_data: u32,
pub flags: u32,
pub time: u32,
pub extra_info: usize,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct KeybdInput {
pub vk: u16,
pub scan: u16,
pub flags: u32,
pub time: u32,
pub extra_info: usize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union InputBody {
pub mouse: MouseInput,
pub keyboard: KeybdInput,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Input {
pub kind: u32,
pub body: InputBody,
}
#[cfg(target_pointer_width = "64")]
const fn field_size<T, F>(_select: fn(&T) -> &F) -> usize {
size_of::<F>()
}
#[cfg(target_pointer_width = "64")]
macro_rules! assert_fields {
($ty:ident { $($field:ident: $offset:literal, $width:literal;)+ }) => {
let _every_field_has_a_row: fn($ty) = |value| {
let $ty { $($field: _),+ } = value;
};
$(
assert!(
core::mem::offset_of!($ty, $field) == $offset,
concat!("offset of ", stringify!($ty), "::", stringify!($field))
);
assert!(
field_size(|value: &$ty| &value.$field) == $width,
concat!("width of ", stringify!($ty), "::", stringify!($field))
);
)+
};
}
#[cfg(target_pointer_width = "64")]
const _: () = {
assert!(
size_of::<MouseInput>() == 32,
"MOUSEINPUT on 64-bit Windows"
);
assert!(
size_of::<KeybdInput>() == 24,
"KEYBDINPUT on 64-bit Windows"
);
assert!(size_of::<Input>() == 40, "INPUT on 64-bit Windows");
assert!(align_of::<Input>() == 8, "ULONG_PTR alignment");
assert_fields!(MouseInput {
dx: 0, 4;
dy: 4, 4;
mouse_data: 8, 4;
flags: 12, 4;
time: 16, 4;
extra_info: 24, 8;
});
assert_fields!(KeybdInput {
vk: 0, 2;
scan: 2, 2;
flags: 4, 4;
time: 8, 4;
extra_info: 16, 8;
});
assert_fields!(Input {
kind: 0, 4;
body: 8, 32;
});
assert!(core::mem::offset_of!(Input, body.mouse) == 8);
assert!(core::mem::offset_of!(Input, body.keyboard) == 8);
};
pub const INPUT_MOUSE: u32 = 0;
pub const INPUT_KEYBOARD: u32 = 1;
pub const KEYEVENTF_EXTENDED: u32 = 0x0001;
pub const KEYEVENTF_KEY_UP: u32 = 0x0002;
pub const KEYEVENTF_SCANCODE: u32 = 0x0008;
pub const MOUSEEVENTF_MOVE: u32 = 0x0001;
pub const MOUSEEVENTF_LEFT_DOWN: u32 = 0x0002;
pub const MOUSEEVENTF_LEFT_UP: u32 = 0x0004;
pub const MOUSEEVENTF_RIGHT_DOWN: u32 = 0x0008;
pub const MOUSEEVENTF_RIGHT_UP: u32 = 0x0010;
pub const MOUSEEVENTF_MIDDLE_DOWN: u32 = 0x0020;
pub const MOUSEEVENTF_MIDDLE_UP: u32 = 0x0040;
pub const MOUSEEVENTF_X_DOWN: u32 = 0x0080;
pub const MOUSEEVENTF_X_UP: u32 = 0x0100;
pub const MOUSEEVENTF_WHEEL: u32 = 0x0800;
pub const MOUSEEVENTF_ABSOLUTE: u32 = 0x8000;
pub const XBUTTON1: u32 = 0x0001;
pub const XBUTTON2: u32 = 0x0002;
pub const ABSOLUTE_MAX: i32 = 65_535;
pub const WHEEL_DELTA: i32 = 120;
pub const EXTENDED_PREFIX: u32 = 0xE000;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct Point {
pub x: i32,
pub y: i32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct Rect {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct PointerInfo {
pub pointer_type: i32,
pub pointer_id: u32,
pub frame_id: u32,
pub pointer_flags: u32,
pub source_device: *mut c_void,
pub hwnd_target: Handle,
pub pixel_location: Point,
pub himetric_location: Point,
pub pixel_location_raw: Point,
pub himetric_location_raw: Point,
pub time: u32,
pub history_count: u32,
pub input_data: i32,
pub key_states: u32,
pub performance_count: u64,
pub button_change_type: i32,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct PointerTouchInfo {
pub pointer_info: PointerInfo,
pub touch_flags: u32,
pub touch_mask: u32,
pub contact: Rect,
pub contact_raw: Rect,
pub orientation: u32,
pub pressure: u32,
}
#[cfg(target_pointer_width = "64")]
const _: () = {
assert!(
size_of::<PointerInfo>() == 96,
"POINTER_INFO on 64-bit Windows"
);
assert!(
size_of::<PointerTouchInfo>() == 144,
"POINTER_TOUCH_INFO on 64-bit Windows"
);
assert!(size_of::<Point>() == 8, "POINT");
assert!(size_of::<Rect>() == 16, "RECT");
assert_fields!(Point {
x: 0, 4;
y: 4, 4;
});
assert_fields!(Rect {
left: 0, 4;
top: 4, 4;
right: 8, 4;
bottom: 12, 4;
});
assert_fields!(PointerInfo {
pointer_type: 0, 4;
pointer_id: 4, 4;
frame_id: 8, 4;
pointer_flags: 12, 4;
source_device: 16, 8;
hwnd_target: 24, 8;
pixel_location: 32, 8;
himetric_location: 40, 8;
pixel_location_raw: 48, 8;
himetric_location_raw: 56, 8;
time: 64, 4;
history_count: 68, 4;
input_data: 72, 4;
key_states: 76, 4;
performance_count: 80, 8;
button_change_type: 88, 4;
});
assert_fields!(PointerTouchInfo {
pointer_info: 0, 96;
touch_flags: 96, 4;
touch_mask: 100, 4;
contact: 104, 16;
contact_raw: 120, 16;
orientation: 136, 4;
pressure: 140, 4;
});
};
pub const PT_TOUCH: i32 = 2;
pub const TOUCH_FEEDBACK_NONE: u32 = 0x3;
pub const MAX_CONTACTS: u32 = 10;
pub const POINTER_FLAG_IN_RANGE: u32 = 0x0000_0002;
pub const POINTER_FLAG_IN_CONTACT: u32 = 0x0000_0004;
pub const POINTER_FLAG_DOWN: u32 = 0x0001_0000;
pub const POINTER_FLAG_UPDATE: u32 = 0x0002_0000;
pub const POINTER_FLAG_UP: u32 = 0x0004_0000;
pub const DPI_PER_MONITOR_AWARE_V2: isize = -4;
#[link(name = "user32")]
unsafe extern "system" {
pub fn InitializeTouchInjection(max_count: u32, feedback: u32) -> i32;
pub fn InjectTouchInput(count: u32, contacts: *const PointerTouchInfo) -> i32;
pub fn SetProcessDpiAwarenessContext(context: isize) -> i32;
pub fn SendInput(count: u32, inputs: *const Input, size: i32) -> u32;
pub fn GetForegroundWindow() -> Handle;
}
#[link(name = "kernel32")]
unsafe extern "system" {
pub fn GetLastError() -> u32;
}
}
#[cfg(target_os = "windows")]
fn main() -> ExitCode {
say(&format!("ready fg={}", foreground()));
let mut fingers = Fingers::default();
for line in std::io::stdin().lock().lines() {
let line = match line {
Ok(line) => line,
Err(error) => {
eprintln!("crcbl-e2e-win32-input: could not read stdin: {error}");
return ExitCode::FAILURE;
}
};
let command = line.trim();
if command.is_empty() {
continue;
}
match run(command, &mut fingers) {
Ok(()) => say(&format!("sent {command:?} fg={}", foreground())),
Err(problem) => {
eprintln!("crcbl-e2e-win32-input: {command:?}: {problem}");
return ExitCode::from(2);
}
}
}
ExitCode::SUCCESS
}
#[cfg(target_os = "windows")]
fn run(command: &str, fingers: &mut Fingers) -> Result<(), String> {
let mut words = command.split_whitespace();
let verb = words.next().unwrap_or_default();
match verb {
"key" | "down" | "up" => {
let scancode = scancode(words.next())?;
match verb {
"down" => key(scancode, true),
"up" => key(scancode, false),
_ => key(scancode, true).and_then(|()| key(scancode, false)),
}
}
"vkdown" | "vkup" => {
let vk = number(words.next(), "virtual key")?;
let vk = u16::try_from(vk).map_err(|_| format!("{vk} is not a virtual key"))?;
virtual_key(vk, verb == "vkdown")
}
"move" => {
let dx = number(words.next(), "dx")?;
let dy = number(words.next(), "dy")?;
mouse(win32::MOUSEEVENTF_MOVE, 0, dx, dy)
}
"abs" => {
let x = number(words.next(), "x")?;
let y = number(words.next(), "y")?;
let range = 0..=win32::ABSOLUTE_MAX;
if !range.contains(&x) || !range.contains(&y) {
return Err(format!(
"({x}, {y}) is outside the normalized range {range:?}"
));
}
mouse(
win32::MOUSEEVENTF_MOVE | win32::MOUSEEVENTF_ABSOLUTE,
0,
x,
y,
)
}
"click" => {
let (down, up, mouse_data) = match words.next() {
Some("left") => (win32::MOUSEEVENTF_LEFT_DOWN, win32::MOUSEEVENTF_LEFT_UP, 0),
Some("right") => (
win32::MOUSEEVENTF_RIGHT_DOWN,
win32::MOUSEEVENTF_RIGHT_UP,
0,
),
Some("middle") => (
win32::MOUSEEVENTF_MIDDLE_DOWN,
win32::MOUSEEVENTF_MIDDLE_UP,
0,
),
Some("back") => (
win32::MOUSEEVENTF_X_DOWN,
win32::MOUSEEVENTF_X_UP,
win32::XBUTTON1,
),
Some("forward") => (
win32::MOUSEEVENTF_X_DOWN,
win32::MOUSEEVENTF_X_UP,
win32::XBUTTON2,
),
other => {
return Err(format!(
"{other:?} is not left, right, middle, back or forward"
));
}
};
mouse(down, mouse_data, 0, 0).and_then(|()| mouse(up, mouse_data, 0, 0))
}
"wheel" => {
let notches: i32 = number(words.next(), "notches")?;
let delta = notches
.checked_mul(win32::WHEEL_DELTA)
.ok_or("that many notches does not fit in a wheel delta")?;
mouse(win32::MOUSEEVENTF_WHEEL, delta as u32, 0, 0)
}
"touch" => {
let action = words.next();
let id = u32::try_from(number(words.next(), "finger id")?)
.map_err(|_| "a finger id is not negative")?;
match action {
Some("down") => {
let at = point(&mut words)?;
fingers.down(id, at)
}
Some("move") => {
let at = point(&mut words)?;
fingers.moved(id, at)
}
Some("up") => fingers.up(id),
other => Err(format!("{other:?} is not down, move or up")),
}
}
other => Err(format!(
"{other:?} is not one of key, down, up, move, abs, click, wheel, touch"
)),
}
}
#[cfg(target_os = "windows")]
fn point<'a>(words: &mut impl Iterator<Item = &'a str>) -> Result<win32::Point, String> {
let x = number(words.next(), "x")?;
let y = number(words.next(), "y")?;
Ok(win32::Point { x, y })
}
#[cfg(target_os = "windows")]
#[derive(Default)]
struct Fingers {
ready: bool,
down: Vec<(u32, win32::Point)>,
}
#[cfg(target_os = "windows")]
impl Fingers {
fn down(&mut self, id: u32, at: win32::Point) -> Result<(), String> {
if self.down.iter().any(|&(finger, _)| finger == id) {
return Err(format!("finger {id} is already down"));
}
let flags = win32::POINTER_FLAG_DOWN
| win32::POINTER_FLAG_IN_RANGE
| win32::POINTER_FLAG_IN_CONTACT;
self.inject(id, at, flags)?;
self.down.push((id, at));
Ok(())
}
fn moved(&mut self, id: u32, at: win32::Point) -> Result<(), String> {
let index = self.index(id)?;
let flags = win32::POINTER_FLAG_UPDATE
| win32::POINTER_FLAG_IN_RANGE
| win32::POINTER_FLAG_IN_CONTACT;
self.inject(id, at, flags)?;
self.down[index].1 = at;
Ok(())
}
fn up(&mut self, id: u32) -> Result<(), String> {
let index = self.index(id)?;
let at = self.down[index].1;
self.inject(id, at, win32::POINTER_FLAG_UP)?;
self.down.remove(index);
Ok(())
}
fn index(&self, id: u32) -> Result<usize, String> {
self.down
.iter()
.position(|&(finger, _)| finger == id)
.ok_or_else(|| format!("finger {id} is not down"))
}
fn inject(&mut self, id: u32, at: win32::Point, flags: u32) -> Result<(), String> {
if !self.ready {
unsafe { win32::SetProcessDpiAwarenessContext(win32::DPI_PER_MONITOR_AWARE_V2) };
if unsafe {
win32::InitializeTouchInjection(win32::MAX_CONTACTS, win32::TOUCH_FEEDBACK_NONE)
} == 0
{
return Err(last_error("InitializeTouchInjection"));
}
self.ready = true;
}
let holding = win32::POINTER_FLAG_UPDATE
| win32::POINTER_FLAG_IN_RANGE
| win32::POINTER_FLAG_IN_CONTACT;
let frame: Vec<win32::PointerTouchInfo> = self
.down
.iter()
.filter(|&&(finger, _)| finger != id)
.map(|&(finger, still)| contact(finger, still, holding))
.chain([contact(id, at, flags)])
.collect();
let count = u32::try_from(frame.len()).expect("at most MAX_CONTACTS fingers");
if unsafe { win32::InjectTouchInput(count, frame.as_ptr()) } == 0 {
return Err(last_error("InjectTouchInput"));
}
Ok(())
}
}
#[cfg(target_os = "windows")]
fn contact(id: u32, at: win32::Point, flags: u32) -> win32::PointerTouchInfo {
win32::PointerTouchInfo {
pointer_info: win32::PointerInfo {
pointer_type: win32::PT_TOUCH,
pointer_id: id,
frame_id: 0,
pointer_flags: flags,
source_device: core::ptr::null_mut(),
hwnd_target: core::ptr::null_mut(),
pixel_location: at,
himetric_location: win32::Point::default(),
pixel_location_raw: win32::Point::default(),
himetric_location_raw: win32::Point::default(),
time: 0,
history_count: 0,
input_data: 0,
key_states: 0,
performance_count: 0,
button_change_type: 0,
},
touch_flags: 0,
touch_mask: 0,
contact: win32::Rect::default(),
contact_raw: win32::Rect::default(),
orientation: 0,
pressure: 0,
}
}
#[cfg(target_os = "windows")]
fn last_error(what: &str) -> String {
let error = unsafe { win32::GetLastError() };
format!("{what} failed with Win32 error {error}")
}
#[cfg(target_os = "windows")]
fn scancode(word: Option<&str>) -> Result<u32, String> {
let word = word.ok_or("a scan code is required")?;
let code = if let Some(hex) = word.strip_prefix("0x") {
u32::from_str_radix(hex, 16).map_err(|_| format!("{word:?} is not a hex scan code"))?
} else {
word.parse::<u32>()
.map_err(|_| format!("{word:?} is not a scan code"))?
};
let extended = code & win32::EXTENDED_PREFIX == win32::EXTENDED_PREFIX;
let low = code & !win32::EXTENDED_PREFIX;
if low > 0xFF || (!extended && code > 0xFF) {
return Err(format!(
"{code:#06x} is neither a set 1 scan code nor an 0xE0-prefixed one"
));
}
Ok(code)
}
#[cfg(target_os = "windows")]
fn number(word: Option<&str>, what: &str) -> Result<i32, String> {
word.ok_or_else(|| format!("{what} is required"))?
.parse::<i32>()
.map_err(|_| format!("{what} must be a whole number"))
}
#[cfg(target_os = "windows")]
fn key(scancode: u32, pressed: bool) -> Result<(), String> {
let extended = scancode & win32::EXTENDED_PREFIX == win32::EXTENDED_PREFIX;
let mut flags = win32::KEYEVENTF_SCANCODE;
if extended {
flags |= win32::KEYEVENTF_EXTENDED;
}
if !pressed {
flags |= win32::KEYEVENTF_KEY_UP;
}
send(win32::Input {
kind: win32::INPUT_KEYBOARD,
body: win32::InputBody {
keyboard: win32::KeybdInput {
vk: 0,
scan: (scancode & 0xFF) as u16,
flags,
..win32::KeybdInput::default()
},
},
})
}
#[cfg(target_os = "windows")]
fn virtual_key(vk: u16, pressed: bool) -> Result<(), String> {
let flags = if pressed { 0 } else { win32::KEYEVENTF_KEY_UP };
send(win32::Input {
kind: win32::INPUT_KEYBOARD,
body: win32::InputBody {
keyboard: win32::KeybdInput {
vk,
scan: 0,
flags,
..win32::KeybdInput::default()
},
},
})
}
#[cfg(target_os = "windows")]
fn mouse(flags: u32, mouse_data: u32, dx: i32, dy: i32) -> Result<(), String> {
send(win32::Input {
kind: win32::INPUT_MOUSE,
body: win32::InputBody {
mouse: win32::MouseInput {
dx,
dy,
mouse_data,
flags,
..win32::MouseInput::default()
},
},
})
}
#[cfg(target_os = "windows")]
fn send(input: win32::Input) -> Result<(), String> {
let size = i32::try_from(size_of::<win32::Input>()).expect("INPUT is 40 bytes");
let inserted = unsafe { win32::SendInput(1, &raw const input, size) };
if inserted == 1 {
return Ok(());
}
let error = unsafe { win32::GetLastError() };
Err(format!(
"SendInput inserted {inserted} of 1 events, Win32 error {error}; the session may have no \
interactive desktop, or a higher-integrity window may hold the foreground"
))
}
#[cfg(target_os = "windows")]
fn foreground() -> String {
format!("{:#x}", unsafe { win32::GetForegroundWindow() } as usize)
}
#[cfg(target_os = "windows")]
fn say(what: &str) {
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "crcbl-e2e-win32-input: {what}");
let _ = out.flush();
}
#[cfg(all(test, target_os = "windows"))]
#[path = "../../src/win32/sdk_witness.rs"]
mod sdk_witness;
#[cfg(all(test, target_os = "windows"))]
mod sdk_layout {
use crate::sdk_witness::{Witnessed, witness};
use crate::win32::{
Input, InputBody, KeybdInput, MouseInput, Point, PointerInfo, PointerTouchInfo, Rect,
};
use windows::Win32::Foundation::{POINT, RECT};
use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, INPUT_0, KEYBDINPUT, MOUSEINPUT};
use windows::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_TOUCH_INFO};
witness!(mouse_input_matches_the_sdk: struct MouseInput => MOUSEINPUT {
dx: is dx;
dy: is dy;
mouse_data: is mouseData;
flags: is dwFlags.0;
time: is time;
extra_info: is dwExtraInfo;
});
witness!(keybd_input_matches_the_sdk: struct KeybdInput => KEYBDINPUT {
vk: is wVk.0;
scan: is wScan;
flags: is dwFlags.0;
time: is time;
extra_info: is dwExtraInfo;
});
witness!(input_body_matches_the_sdk: union InputBody => INPUT_0 {
mouse: nested mi;
keyboard: nested ki;
});
witness!(input_matches_the_sdk: struct Input => INPUT {
kind: is r#type.0;
body: nested Anonymous;
});
witness!(point_matches_the_sdk: struct Point => POINT {
x: is x;
y: is y;
});
witness!(rect_matches_the_sdk: struct Rect => RECT {
left: is left;
top: is top;
right: is right;
bottom: is bottom;
});
witness!(pointer_info_matches_the_sdk: struct PointerInfo => POINTER_INFO {
pointer_type: is pointerType.0;
pointer_id: is pointerId;
frame_id: is frameId;
pointer_flags: is pointerFlags.0;
source_device: is sourceDevice.0;
hwnd_target: is hwndTarget.0;
pixel_location: nested ptPixelLocation;
himetric_location: nested ptHimetricLocation;
pixel_location_raw: nested ptPixelLocationRaw;
himetric_location_raw: nested ptHimetricLocationRaw;
time: is dwTime;
history_count: is historyCount;
input_data: is InputData;
key_states: is dwKeyStates;
performance_count: is PerformanceCount;
button_change_type: is ButtonChangeType.0;
});
witness!(pointer_touch_info_matches_the_sdk: struct PointerTouchInfo => POINTER_TOUCH_INFO {
pointer_info: nested pointerInfo;
touch_flags: is touchFlags;
touch_mask: is touchMask;
contact: nested rcContact;
contact_raw: nested rcContactRaw;
orientation: is orientation;
pressure: is pressure;
});
}