Pure move of the 1963-line module into input/mod.rs (backend and capability negotiation), input/acl.rs (fail-closed /dev/uinput ACL validation), input/keymap.rs (US keymap and gamepad tables), and input/linux.rs (uinput device).
- commit
- 75994737592f16b943ec78c3b798b408184a225a
- parent
- c8aee34572b5ba501e832537e5cf12d2c4088daa
- author
- Dami <dami.etoile@gmail.com>
| D | crates/ | 1963 | ------------------------------ |
| A | crates/ | 193 | +++ |
| A | crates/ | 745 | +++++++++++ |
| A | crates/ | 691 | +++++++++++ |
| A | crates/ | 351 | +++++ |
@@ -1,1963 +0,0 @@
1-use std::fmt;2-3-use std::mem::size_of;4-5-use std::sync::Arc;6-7-use std::sync::Mutex;8-9-10-use anyhow::Context;11-use anyhow::{Result, bail};12-13-use sigil_protocol::GamepadState;14-use sigil_protocol::{Capability, InputEvent};15-16-use crate::config::{HostConfig, InputMode};17-18-const ACK_ONLY_CAPABILITIES: &[Capability] = &[Capability::InputAck];19-const UINPUT_CAPABILITIES: &[Capability] = &[20- Capability::RelativePointer,21- Capability::Keyboard,22- Capability::Gamepad,23- Capability::InputAck,24-];25-26-27-const POSIX_ACL_XATTR_VERSION: u32 = 0x0002;28-29-const POSIX_ACL_ENTRY_BYTES: usize = 8;30-31-const POSIX_ACL_REQUIRED_ENTRIES: usize = 5;32-33-const POSIX_ACL_REQUIRED_BYTES: usize =34- size_of::<u32>() + POSIX_ACL_ENTRY_BYTES * POSIX_ACL_REQUIRED_ENTRIES;35-36-const POSIX_ACL_UNDEFINED_ID: u32 = u32::MAX;37-38-const POSIX_ACL_USER_OBJ: u16 = 0x01;39-40-const POSIX_ACL_USER: u16 = 0x02;41-42-const POSIX_ACL_GROUP_OBJ: u16 = 0x04;43-44-const POSIX_ACL_MASK: u16 = 0x10;45-46-const POSIX_ACL_OTHER: u16 = 0x20;47-48-const POSIX_ACL_READ_WRITE: u16 = 0x06;49-50-/// Validate the only extended uinput ACL shape Sigil accepts.51-///52-/// Linux stores POSIX ACL xattrs as one little-endian version followed by53-/// fixed-size entries. Requiring the canonical five-entry form prevents a54-/// configured one-user exception from admitting extra users or groups.55-56-fn validate_single_user_access_acl(57- bytes: &[u8],58- expected_uid: u32,59- expected_mode: u32,60-) -> Result<()> {61- if bytes.len() != POSIX_ACL_REQUIRED_BYTES {62- bail!(63- "configured uinput access ACL must contain exactly one named user and no named groups"64- );65- }66- let version = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);67- if version != POSIX_ACL_XATTR_VERSION {68- bail!("configured uinput access ACL has an unsupported Linux ACL version");69- }70-71- let expected_entries = [72- (73- POSIX_ACL_USER_OBJ,74- ((expected_mode >> 6) & 0o7) as u16,75- POSIX_ACL_UNDEFINED_ID,76- ),77- (POSIX_ACL_USER, POSIX_ACL_READ_WRITE, expected_uid),78- (79- POSIX_ACL_GROUP_OBJ,80- ((expected_mode >> 3) & 0o7) as u16,81- POSIX_ACL_UNDEFINED_ID,82- ),83- (84- POSIX_ACL_MASK,85- ((expected_mode >> 3) & 0o7) as u16,86- POSIX_ACL_UNDEFINED_ID,87- ),88- (89- POSIX_ACL_OTHER,90- (expected_mode & 0o7) as u16,91- POSIX_ACL_UNDEFINED_ID,92- ),93- ];94-95- for (index, (expected_tag, expected_permissions, expected_id)) in96- expected_entries.into_iter().enumerate()97- {98- let offset = size_of::<u32>() + index * POSIX_ACL_ENTRY_BYTES;99- let tag = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);100- let permissions = u16::from_le_bytes([bytes[offset + 2], bytes[offset + 3]]);101- let id = u32::from_le_bytes([102- bytes[offset + 4],103- bytes[offset + 5],104- bytes[offset + 6],105- bytes[offset + 7],106- ]);107- if permissions & !0o7 != 0 {108- bail!("configured uinput access ACL contains invalid permission bits");109- }110- if tag != expected_tag || permissions != expected_permissions || id != expected_id {111- bail!(112- "configured uinput access ACL does not match the exact one-user owner/group/mode contract"113- );114- }115- }116- Ok(())117-}118-119-120-// This Linux input identity tuple is an external ABI consumed by Gamescope,121-// udev/libinput rules, and hardware evidence. Preserve it exactly; see122-// docs/compatibility-identifiers.md.123-const UINPUT_BUS_TYPE: u16 = 0x06; // Linux BUS_VIRTUAL124-125-const UINPUT_VENDOR_ID: u16 = 0x5347;126-127-const UINPUT_DEVICE_VERSION: u16 = 1;128-129-const POINTER_PRODUCT_ID: u16 = 1;130-131-const GAMEPAD_PRODUCT_ID: u16 = 2;132-133-const KEYBOARD_PRODUCT_ID: u16 = 3;134-135-const POINTER_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Pointer";136-137-const KEYBOARD_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Keyboard";138-139-const GAMEPAD_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Gamepad";140-141-142-143-enum InputDeviceClass {144- Pointer,145- Keyboard,146- Gamepad,147-}148-149-150-151-enum PointerReportEvent {152- RelativeX(i32),153- RelativeY(i32),154- Synchronize,155-}156-157-158-/// Emulate an absolute position on the relative-only appliance pointer.159-///160-/// This is correct only when Gamescope clamps the first report at the surface161-/// origin and applies neither acceleration nor scaling to either report. The162-/// hardware UAT verifies both the synchronized position and one subsequent163-/// relative delta through the Xwayland pointer tracker.164-fn pointer_position_sync_report(x: i32, y: i32) -> [PointerReportEvent; 6] {165- [166- PointerReportEvent::RelativeX(sigil_protocol::RELATIVE_POINTER_DELTA_MIN),167- PointerReportEvent::RelativeY(sigil_protocol::RELATIVE_POINTER_DELTA_MIN),168- PointerReportEvent::Synchronize,169- PointerReportEvent::RelativeX(x),170- PointerReportEvent::RelativeY(y),171- PointerReportEvent::Synchronize,172- ]173-}174-175-176-fn input_device_class(event: &InputEvent) -> Option<InputDeviceClass> {177- match event {178- InputEvent::Probe => None,179- InputEvent::MouseMove { .. }180- | InputEvent::MouseMoveRelative { .. }181- | InputEvent::MousePositionSync { .. }182- | InputEvent::MouseClick { .. }183- | InputEvent::MouseDown { .. }184- | InputEvent::MouseUp { .. }185- | InputEvent::MouseScroll { .. } => Some(InputDeviceClass::Pointer),186- InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } | InputEvent::KeyClick { .. } => {187- Some(InputDeviceClass::Keyboard)188- }189- InputEvent::Gamepad { .. } => Some(InputDeviceClass::Gamepad),190- InputEvent::Text { .. } => None,191- }192-}193-194-195-pub enum InputDisposition {196- Probed,197- Disabled,198- Observed,199- 200- Injected,201- TextIgnored,202-}203-204-205-pub struct InputBackend {206- mode: InputMode,207- 208- device: Option<Arc<std::sync::Mutex<linux::UinputDevice>>>,209-}210-211-impl fmt::Debug for InputBackend {212- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {213- formatter214- .debug_struct("InputBackend")215- .field("mode", &self.mode)216- .finish_non_exhaustive()217- }218-}219-220-impl InputBackend {221- /// Construct the backend before the network endpoint is exposed. For222- /// uinput this opens, validates, and registers the virtual device.223- pub fn initialize(config: &HostConfig) -> Result<Self> {224- match config.input_mode {225- InputMode::Disabled | InputMode::Log => Ok(Self {226- mode: config.input_mode.clone(),227- 228- device: None,229- }),230- InputMode::Uinput => {231- 232- {233- let settings = config234- .uinput235- .as_ref()236- .context("missing validated uinput configuration")?;237- let device = linux::UinputDevice::open(settings)?;238- Ok(Self {239- mode: InputMode::Uinput,240- device: Some(Arc::new(std::sync::Mutex::new(device))),241- })242- }243- 244- {245- let _ = config;246- bail!("input_mode uinput is supported only on Linux")247- }248- }249- }250- }251-252- pub fn capabilities(&self) -> &'static [Capability] {253- capabilities_for_mode(&self.mode)254- }255-256- pub fn apply(&self, event: &InputEvent, negotiated: &[Capability]) -> Result<InputDisposition> {257- ensure_event_was_negotiated(&self.mode, event, negotiated)?;258- if matches!(event, InputEvent::Probe) {259- return Ok(InputDisposition::Probed);260- }261- match self.mode {262- InputMode::Disabled => Ok(InputDisposition::Disabled),263- InputMode::Log => Ok(InputDisposition::Observed),264- InputMode::Uinput => {265- if matches!(event, InputEvent::Text { .. }) {266- return Ok(InputDisposition::TextIgnored);267- }268- 269- {270- let device = self271- .device272- .as_ref()273- .context("uinput backend was not initialized")?;274- device275- .lock()276- .map_err(|_| anyhow::anyhow!("uinput backend lock poisoned"))?277- .apply(event)?;278- Ok(InputDisposition::Injected)279- }280- 281- {282- let _ = event;283- bail!("uinput backend is unavailable on this platform")284- }285- }286- }287- }288-289- /// Release any transitions held by the ending input session so a dropped290- /// connection cannot leave a key or mouse button pressed.291- pub fn reset_session(&self) -> Result<()> {292- 293- if let Some(device) = &self.device {294- reset_through_poisoned_lock(device, linux::UinputDevice::release_all)?;295- }296- Ok(())297- }298-}299-300-301-fn reset_through_poisoned_lock<T>(302- device: &Mutex<T>,303- reset: impl FnOnce(&mut T) -> Result<()>,304-) -> Result<()> {305- let mut device = match device.lock() {306- Ok(device) => device,307- Err(poisoned) => {308- // A panic while applying input poisons this mutex. Teardown must309- // still recover the device solely to release held transitions;310- // ordinary input continues to reject the poisoned lock.311- tracing::warn!("recovering poisoned uinput lock for session reset");312- poisoned.into_inner()313- }314- };315- reset(&mut device)316-}317-318-fn ensure_event_was_negotiated(319- mode: &InputMode,320- event: &InputEvent,321- negotiated: &[Capability],322-) -> Result<()> {323- if !matches!(mode, InputMode::Uinput) {324- return Ok(());325- }326- let required = match event {327- InputEvent::Probe => Some(Capability::InputAck),328- InputEvent::MouseMove { .. } => Some(Capability::AbsolutePointer),329- InputEvent::MouseMoveRelative { .. }330- | InputEvent::MousePositionSync { .. }331- | InputEvent::MouseClick { .. }332- | InputEvent::MouseDown { .. }333- | InputEvent::MouseUp { .. }334- | InputEvent::MouseScroll { .. } => Some(Capability::RelativePointer),335- InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } | InputEvent::KeyClick { .. } => {336- Some(Capability::Keyboard)337- }338- InputEvent::Gamepad { .. } => Some(Capability::Gamepad),339- // Text remains an explicit content-free no-op for compatibility with340- // clients that emit it alongside negotiated physical key transitions.341- InputEvent::Text { .. } => None,342- };343- if required.is_some_and(|capability| !negotiated.contains(&capability)) {344- bail!("input event class was not negotiated for this session");345- }346- Ok(())347-}348-349-fn capabilities_for_mode(mode: &InputMode) -> &'static [Capability] {350- match mode {351- InputMode::Disabled | InputMode::Log => ACK_ONLY_CAPABILITIES,352- InputMode::Uinput => UINPUT_CAPABILITIES,353- }354-}355-356-357-358-enum MappedKey {359- A,360- B,361- C,362- D,363- E,364- F,365- G,366- H,367- I,368- J,369- K,370- L,371- M,372- N,373- O,374- P,375- Q,376- R,377- S,378- T,379- U,380- V,381- W,382- X,383- Y,384- Z,385- Num0,386- Num1,387- Num2,388- Num3,389- Num4,390- Num5,391- Num6,392- Num7,393- Num8,394- Num9,395- Minus,396- Equal,397- LeftBrace,398- RightBrace,399- Backslash,400- Semicolon,401- Apostrophe,402- Grave,403- Comma,404- Dot,405- Slash,406- NonUsBackslash,407- Ro,408- Yen,409- Enter,410- Tab,411- Space,412- Backspace,413- Escape,414- LeftShift,415- RightShift,416- LeftCtrl,417- RightCtrl,418- LeftAlt,419- RightAlt,420- LeftMeta,421- RightMeta,422- Up,423- Down,424- Left,425- Right,426- Home,427- End,428- PageUp,429- PageDown,430- Insert,431- Delete,432- PrintScreen,433- F1,434- F2,435- F3,436- F4,437- F5,438- F6,439- F7,440- F8,441- F9,442- F10,443- F11,444- F12,445-}446-447-448-const ALL_MAPPED_KEYS: &[MappedKey] = &[449- MappedKey::A,450- MappedKey::B,451- MappedKey::C,452- MappedKey::D,453- MappedKey::E,454- MappedKey::F,455- MappedKey::G,456- MappedKey::H,457- MappedKey::I,458- MappedKey::J,459- MappedKey::K,460- MappedKey::L,461- MappedKey::M,462- MappedKey::N,463- MappedKey::O,464- MappedKey::P,465- MappedKey::Q,466- MappedKey::R,467- MappedKey::S,468- MappedKey::T,469- MappedKey::U,470- MappedKey::V,471- MappedKey::W,472- MappedKey::X,473- MappedKey::Y,474- MappedKey::Z,475- MappedKey::Num0,476- MappedKey::Num1,477- MappedKey::Num2,478- MappedKey::Num3,479- MappedKey::Num4,480- MappedKey::Num5,481- MappedKey::Num6,482- MappedKey::Num7,483- MappedKey::Num8,484- MappedKey::Num9,485- MappedKey::Minus,486- MappedKey::Equal,487- MappedKey::LeftBrace,488- MappedKey::RightBrace,489- MappedKey::Backslash,490- MappedKey::Semicolon,491- MappedKey::Apostrophe,492- MappedKey::Grave,493- MappedKey::Comma,494- MappedKey::Dot,495- MappedKey::Slash,496- MappedKey::NonUsBackslash,497- MappedKey::Ro,498- MappedKey::Yen,499- MappedKey::Enter,500- MappedKey::Tab,501- MappedKey::Space,502- MappedKey::Backspace,503- MappedKey::Escape,504- MappedKey::LeftShift,505- MappedKey::RightShift,506- MappedKey::LeftCtrl,507- MappedKey::RightCtrl,508- MappedKey::LeftAlt,509- MappedKey::RightAlt,510- MappedKey::LeftMeta,511- MappedKey::RightMeta,512- MappedKey::Up,513- MappedKey::Down,514- MappedKey::Left,515- MappedKey::Right,516- MappedKey::Home,517- MappedKey::End,518- MappedKey::PageUp,519- MappedKey::PageDown,520- MappedKey::Insert,521- MappedKey::Delete,522- MappedKey::PrintScreen,523- MappedKey::F1,524- MappedKey::F2,525- MappedKey::F3,526- MappedKey::F4,527- MappedKey::F5,528- MappedKey::F6,529- MappedKey::F7,530- MappedKey::F8,531- MappedKey::F9,532- MappedKey::F10,533- MappedKey::F11,534- MappedKey::F12,535-];536-537-538-fn map_key(value: &str) -> Option<MappedKey> {539- match value {540- "KeyA" => Some(MappedKey::A),541- "KeyB" => Some(MappedKey::B),542- "KeyC" => Some(MappedKey::C),543- "KeyD" => Some(MappedKey::D),544- "KeyE" => Some(MappedKey::E),545- "KeyF" => Some(MappedKey::F),546- "KeyG" => Some(MappedKey::G),547- "KeyH" => Some(MappedKey::H),548- "KeyI" => Some(MappedKey::I),549- "KeyJ" => Some(MappedKey::J),550- "KeyK" => Some(MappedKey::K),551- "KeyL" => Some(MappedKey::L),552- "KeyM" => Some(MappedKey::M),553- "KeyN" => Some(MappedKey::N),554- "KeyO" => Some(MappedKey::O),555- "KeyP" => Some(MappedKey::P),556- "KeyQ" => Some(MappedKey::Q),557- "KeyR" => Some(MappedKey::R),558- "KeyS" => Some(MappedKey::S),559- "KeyT" => Some(MappedKey::T),560- "KeyU" => Some(MappedKey::U),561- "KeyV" => Some(MappedKey::V),562- "KeyW" => Some(MappedKey::W),563- "KeyX" => Some(MappedKey::X),564- "KeyY" => Some(MappedKey::Y),565- "KeyZ" => Some(MappedKey::Z),566- "Digit0" => Some(MappedKey::Num0),567- "Digit1" => Some(MappedKey::Num1),568- "Digit2" => Some(MappedKey::Num2),569- "Digit3" => Some(MappedKey::Num3),570- "Digit4" => Some(MappedKey::Num4),571- "Digit5" => Some(MappedKey::Num5),572- "Digit6" => Some(MappedKey::Num6),573- "Digit7" => Some(MappedKey::Num7),574- "Digit8" => Some(MappedKey::Num8),575- "Digit9" => Some(MappedKey::Num9),576- "Minus" => Some(MappedKey::Minus),577- "Equal" => Some(MappedKey::Equal),578- "BracketLeft" => Some(MappedKey::LeftBrace),579- "BracketRight" => Some(MappedKey::RightBrace),580- "Backslash" => Some(MappedKey::Backslash),581- "IntlBackslash" => Some(MappedKey::NonUsBackslash),582- "IntlRo" => Some(MappedKey::Ro),583- "IntlYen" => Some(MappedKey::Yen),584- "Semicolon" => Some(MappedKey::Semicolon),585- "Quote" => Some(MappedKey::Apostrophe),586- "Backquote" => Some(MappedKey::Grave),587- "Comma" => Some(MappedKey::Comma),588- "Period" => Some(MappedKey::Dot),589- "Slash" => Some(MappedKey::Slash),590- "Enter" => Some(MappedKey::Enter),591- "Tab" => Some(MappedKey::Tab),592- "Space" => Some(MappedKey::Space),593- "Backspace" => Some(MappedKey::Backspace),594- "Escape" => Some(MappedKey::Escape),595- "ShiftLeft" => Some(MappedKey::LeftShift),596- "ShiftRight" => Some(MappedKey::RightShift),597- "ControlLeft" => Some(MappedKey::LeftCtrl),598- "ControlRight" => Some(MappedKey::RightCtrl),599- "AltLeft" => Some(MappedKey::LeftAlt),600- "AltRight" => Some(MappedKey::RightAlt),601- "MetaLeft" => Some(MappedKey::LeftMeta),602- "MetaRight" => Some(MappedKey::RightMeta),603- "Shift" => Some(MappedKey::LeftShift),604- "Control" => Some(MappedKey::LeftCtrl),605- "Alt" => Some(MappedKey::LeftAlt),606- "Meta" => Some(MappedKey::LeftMeta),607- "ArrowUp" => Some(MappedKey::Up),608- "ArrowDown" => Some(MappedKey::Down),609- "ArrowLeft" => Some(MappedKey::Left),610- "ArrowRight" => Some(MappedKey::Right),611- "Up" => Some(MappedKey::Up),612- "Down" => Some(MappedKey::Down),613- "Left" => Some(MappedKey::Left),614- "Right" => Some(MappedKey::Right),615- "Home" => Some(MappedKey::Home),616- "End" => Some(MappedKey::End),617- "PageUp" => Some(MappedKey::PageUp),618- "PageDown" => Some(MappedKey::PageDown),619- "Insert" => Some(MappedKey::Insert),620- "Delete" => Some(MappedKey::Delete),621- "PrintScreen" => Some(MappedKey::PrintScreen),622- "F1" => Some(MappedKey::F1),623- "F2" => Some(MappedKey::F2),624- "F3" => Some(MappedKey::F3),625- "F4" => Some(MappedKey::F4),626- "F5" => Some(MappedKey::F5),627- "F6" => Some(MappedKey::F6),628- "F7" => Some(MappedKey::F7),629- "F8" => Some(MappedKey::F8),630- "F9" => Some(MappedKey::F9),631- "F10" => Some(MappedKey::F10),632- "F11" => Some(MappedKey::F11),633- "F12" => Some(MappedKey::F12),634- _ => map_printable_us_key(value),635- }636-}637-638-639-fn map_printable_us_key(value: &str) -> Option<MappedKey> {640- let mut chars = value.chars();641- let character = chars.next()?;642- if chars.next().is_some() || !character.is_ascii() {643- return None;644- }645- Some(match character {646- 'a' | 'A' => MappedKey::A,647- 'b' | 'B' => MappedKey::B,648- 'c' | 'C' => MappedKey::C,649- 'd' | 'D' => MappedKey::D,650- 'e' | 'E' => MappedKey::E,651- 'f' | 'F' => MappedKey::F,652- 'g' | 'G' => MappedKey::G,653- 'h' | 'H' => MappedKey::H,654- 'i' | 'I' => MappedKey::I,655- 'j' | 'J' => MappedKey::J,656- 'k' | 'K' => MappedKey::K,657- 'l' | 'L' => MappedKey::L,658- 'm' | 'M' => MappedKey::M,659- 'n' | 'N' => MappedKey::N,660- 'o' | 'O' => MappedKey::O,661- 'p' | 'P' => MappedKey::P,662- 'q' | 'Q' => MappedKey::Q,663- 'r' | 'R' => MappedKey::R,664- 's' | 'S' => MappedKey::S,665- 't' | 'T' => MappedKey::T,666- 'u' | 'U' => MappedKey::U,667- 'v' | 'V' => MappedKey::V,668- 'w' | 'W' => MappedKey::W,669- 'x' | 'X' => MappedKey::X,670- 'y' | 'Y' => MappedKey::Y,671- 'z' | 'Z' => MappedKey::Z,672- '0' | ')' => MappedKey::Num0,673- '1' | '!' => MappedKey::Num1,674- '2' | '@' => MappedKey::Num2,675- '3' | '#' => MappedKey::Num3,676- '4' | '$' => MappedKey::Num4,677- '5' | '%' => MappedKey::Num5,678- '6' | '^' => MappedKey::Num6,679- '7' | '&' => MappedKey::Num7,680- '8' | '*' => MappedKey::Num8,681- '9' | '(' => MappedKey::Num9,682- '-' | '_' => MappedKey::Minus,683- '=' | '+' => MappedKey::Equal,684- '[' | '{' => MappedKey::LeftBrace,685- ']' | '}' => MappedKey::RightBrace,686- '\\' | '|' => MappedKey::Backslash,687- ';' | ':' => MappedKey::Semicolon,688- '\'' | '"' => MappedKey::Apostrophe,689- '`' | '~' => MappedKey::Grave,690- ',' | '<' => MappedKey::Comma,691- '.' | '>' => MappedKey::Dot,692- '/' | '?' => MappedKey::Slash,693- ' ' => MappedKey::Space,694- _ => return None,695- })696-}697-698-699-700-enum GamepadButton {701- A,702- B,703- X,704- Y,705- LeftShoulder,706- RightShoulder,707- Back,708- Start,709- Guide,710- LeftStick,711- RightStick,712-}713-714-715-716-enum GamepadAxis {717- LeftX,718- LeftY,719- RightX,720- RightY,721- LeftTrigger,722- RightTrigger,723- DpadX,724- DpadY,725-}726-727-728-729-struct GamepadReport {730- buttons: [(GamepadButton, bool); 11],731- axes: [(GamepadAxis, i32); 8],732-}733-734-735-fn map_gamepad_state(state: &GamepadState) -> GamepadReport {736- GamepadReport {737- buttons: [738- (GamepadButton::A, state.a),739- (GamepadButton::B, state.b),740- (GamepadButton::X, state.x),741- (GamepadButton::Y, state.y),742- (GamepadButton::LeftShoulder, state.left_shoulder),743- (GamepadButton::RightShoulder, state.right_shoulder),744- (GamepadButton::Back, state.back),745- (GamepadButton::Start, state.start),746- (GamepadButton::Guide, state.guide),747- (GamepadButton::LeftStick, state.left_stick),748- (GamepadButton::RightStick, state.right_stick),749- ],750- axes: [751- (GamepadAxis::LeftX, i32::from(state.left_x)),752- (GamepadAxis::LeftY, i32::from(state.left_y)),753- (GamepadAxis::RightX, i32::from(state.right_x)),754- (GamepadAxis::RightY, i32::from(state.right_y)),755- (GamepadAxis::LeftTrigger, i32::from(state.left_trigger)),756- (GamepadAxis::RightTrigger, i32::from(state.right_trigger)),757- (758- GamepadAxis::DpadX,759- i32::from(state.dpad_right) - i32::from(state.dpad_left),760- ),761- (762- GamepadAxis::DpadY,763- i32::from(state.dpad_down) - i32::from(state.dpad_up),764- ),765- ],766- }767-}768-769-770-mod linux {771- use std::collections::BTreeSet;772- use std::fs::{File, OpenOptions};773- use std::os::fd::AsRawFd;774- use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};775-776- use anyhow::{Context, Result, ensure};777- use input_linux::{778- AbsoluteAxis, AbsoluteEvent, AbsoluteInfo, AbsoluteInfoSetup, EventKind, EventTime,779- InputId, InputProperty, Key, KeyEvent, KeyState, RelativeAxis, RelativeEvent,780- SynchronizeEvent, UInputHandle,781- };782- use sigil_protocol::{783- GAMEPAD_AXIS_MAX, GAMEPAD_AXIS_MIN, GAMEPAD_TRIGGER_MAX, GamepadState,784- InputEvent as ProtocolInputEvent,785- };786-787- use super::{788- ALL_MAPPED_KEYS, GAMEPAD_DEVICE_NAME, GAMEPAD_PRODUCT_ID, GamepadAxis, GamepadButton,789- InputDeviceClass, KEYBOARD_DEVICE_NAME, KEYBOARD_PRODUCT_ID, MappedKey,790- POINTER_DEVICE_NAME, POINTER_PRODUCT_ID, POSIX_ACL_REQUIRED_BYTES, PointerReportEvent,791- UINPUT_BUS_TYPE, UINPUT_DEVICE_VERSION, UINPUT_VENDOR_ID, input_device_class,792- map_gamepad_state, map_key, pointer_position_sync_report, validate_single_user_access_acl,793- };794- use crate::config::UinputConfig;795-796- const UINPUT_MAJOR: u32 = 10;797- const UINPUT_MINOR: u32 = 223;798- const MAX_POINTER_EVENTS: usize = 6;799- const MAX_KEYBOARD_EVENTS: usize = ALL_MAPPED_KEYS.len() + 1;800- const GAMEPAD_REPORT_EVENTS: usize = 20;801-802- pub struct UinputDevice {803- pointer: PointerDevice,804- keyboard: KeyboardDevice,805- gamepad: GamepadDevice,806- }807-808- impl UinputDevice {809- pub fn open(config: &UinputConfig) -> Result<Self> {810- // Each virtual device owns a distinct validated descriptor. This811- // keeps libinput/udev classification from merging keyboard keys812- // into the conventional relative pointer topology.813- let pointer = PointerDevice::open(config)?;814- let keyboard = KeyboardDevice::open(config)?;815- let gamepad = GamepadDevice::open(config)?;816-817- Ok(Self {818- pointer,819- keyboard,820- gamepad,821- })822- }823-824- pub fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {825- match input_device_class(event) {826- Some(InputDeviceClass::Pointer) => self.pointer.apply(event),827- Some(InputDeviceClass::Keyboard) => self.keyboard.apply(event),828- Some(InputDeviceClass::Gamepad) => match event {829- ProtocolInputEvent::Gamepad { state } => self.gamepad.apply(state),830- _ => unreachable!("input device classification is exhaustive"),831- },832- None => Ok(()),833- }834- }835-836- pub fn release_all(&mut self) -> Result<()> {837- // Evaluate every release before combining results so one broken838- // descriptor cannot strand held state on either sibling device.839- let pointer_result = self.pointer.release_all();840- let keyboard_result = self.keyboard.release_all();841- let gamepad_result = self.gamepad.neutralize();842- pointer_result.and(keyboard_result).and(gamepad_result)843- }844- }845-846- struct PointerDevice {847- handle: UInputHandle<File>,848- pressed: BTreeSet<Key>,849- }850-851- impl PointerDevice {852- fn open(config: &UinputConfig) -> Result<Self> {853- let handle = open_validated_handle(config, "pointer")?;854- handle855- .set_propbit(InputProperty::Pointer)856- .context("enabling pointer property")?;857- handle858- .set_evbit(EventKind::Key)859- .context("enabling pointer button events")?;860- for key in [Key::ButtonLeft, Key::ButtonRight, Key::ButtonMiddle] {861- handle862- .set_keybit(key)863- .context("enabling a bounded pointer button capability")?;864- }865- handle866- .set_evbit(EventKind::Relative)867- .context("enabling relative pointer events")?;868- handle869- .set_relbit(RelativeAxis::X)870- .context("enabling relative X events")?;871- handle872- .set_relbit(RelativeAxis::Y)873- .context("enabling relative Y events")?;874- handle875- .set_relbit(RelativeAxis::Wheel)876- .context("enabling vertical wheel events")?;877- handle878- .set_relbit(RelativeAxis::HorizontalWheel)879- .context("enabling horizontal wheel events")?;880- handle881- .create(882- &InputId {883- bustype: UINPUT_BUS_TYPE,884- vendor: UINPUT_VENDOR_ID,885- product: POINTER_PRODUCT_ID,886- version: UINPUT_DEVICE_VERSION,887- },888- POINTER_DEVICE_NAME,889- 0,890- &[],891- )892- .context("registering Sigil virtual pointer")?;893-894- Ok(Self {895- handle,896- pressed: BTreeSet::new(),897- })898- }899-900- fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {901- match event {902- ProtocolInputEvent::MouseMoveRelative { dx, dy } => {903- let mut events = [synchronize_event(); 3];904- let mut len = 0;905- if *dx != 0 {906- events[len] = relative_event(RelativeAxis::X, *dx);907- len += 1;908- }909- if *dy != 0 {910- events[len] = relative_event(RelativeAxis::Y, *dy);911- len += 1;912- }913- if len == 0 {914- return Ok(());915- }916- events[len] = synchronize_event();917- self.emit(&events[..=len])918- }919- ProtocolInputEvent::MousePositionSync { x, y } => {920- let events = pointer_position_sync_report(*x, *y).map(|event| match event {921- PointerReportEvent::RelativeX(value) => {922- relative_event(RelativeAxis::X, value)923- }924- PointerReportEvent::RelativeY(value) => {925- relative_event(RelativeAxis::Y, value)926- }927- PointerReportEvent::Synchronize => synchronize_event(),928- });929- self.emit(&events)930- }931- ProtocolInputEvent::MouseMove { .. } => anyhow::bail!(932- "absolute pointer movement is unavailable on the relative-only uinput device"933- ),934- ProtocolInputEvent::MouseClick { b } => {935- self.click(mouse_button(*b).context("unsupported mouse button")?)936- }937- ProtocolInputEvent::MouseDown { b } => {938- self.press(mouse_button(*b).context("unsupported mouse button")?)939- }940- ProtocolInputEvent::MouseUp { b } => {941- self.release(mouse_button(*b).context("unsupported mouse button")?)942- }943- ProtocolInputEvent::MouseScroll { dx, dy } => {944- let vertical = dy945- .checked_neg()946- .context("vertical wheel delta is out of range")?;947- let mut events = [synchronize_event(); 3];948- let mut len = 0;949- if *dx != 0 {950- events[len] = relative_event(RelativeAxis::HorizontalWheel, *dx);951- len += 1;952- }953- if vertical != 0 {954- events[len] = relative_event(RelativeAxis::Wheel, vertical);955- len += 1;956- }957- if len == 0 {958- return Ok(());959- }960- events[len] = synchronize_event();961- self.emit(&events[..=len])962- }963- _ => unreachable!("only pointer events are routed to the pointer device"),964- }965- }966-967- fn press(&mut self, key: Key) -> Result<()> {968- self.emit(&[key_event(key, KeyState::PRESSED), synchronize_event()])?;969- self.pressed.insert(key);970- Ok(())971- }972-973- fn release(&mut self, key: Key) -> Result<()> {974- self.emit(&[key_event(key, KeyState::RELEASED), synchronize_event()])?;975- self.pressed.remove(&key);976- Ok(())977- }978-979- fn click(&mut self, key: Key) -> Result<()> {980- ensure!(981- !self.pressed.contains(&key),982- "click transition is invalid while the key or button is held"983- );984- self.emit(&[985- key_event(key, KeyState::PRESSED),986- synchronize_event(),987- key_event(key, KeyState::RELEASED),988- synchronize_event(),989- ])990- }991-992- fn release_all(&mut self) -> Result<()> {993- if self.pressed.is_empty() {994- return Ok(());995- }996- let mut events = Vec::with_capacity(self.pressed.len() + 1);997- events.extend(998- self.pressed999- .iter()1000- .copied()1001- .map(|key| key_event(key, KeyState::RELEASED)),1002- );1003- events.push(synchronize_event());1004- ensure!(1005- events.len() <= MAX_POINTER_EVENTS,1006- "pointer release set exceeded its static bound"1007- );1008- self.emit(&events)?;1009- self.pressed.clear();1010- Ok(())1011- }1012-1013- fn emit(&self, events: &[input_linux::sys::input_event]) -> Result<()> {1014- ensure!(1015- !events.is_empty() && events.len() <= MAX_POINTER_EVENTS,1016- "pointer event batch is outside its static bound"1017- );1018- let written = self1019- .handle1020- .write(events)1021- .context("writing nonblocking pointer event batch")?;1022- ensure!(1023- written == events.len(),1024- "uinput accepted only {written} of {} pointer events",1025- events.len()1026- );1027- Ok(())1028- }1029- }1030-1031- impl Drop for PointerDevice {1032- fn drop(&mut self) {1033- let _ = self.release_all();1034- let _ = self.handle.dev_destroy();1035- }1036- }1037-1038- struct KeyboardDevice {1039- handle: UInputHandle<File>,1040- pressed: BTreeSet<Key>,1041- }1042-1043- impl KeyboardDevice {1044- fn open(config: &UinputConfig) -> Result<Self> {1045- let handle = open_validated_handle(config, "keyboard")?;1046- handle1047- .set_evbit(EventKind::Key)1048- .context("enabling keyboard key events")?;1049- for key in ALL_MAPPED_KEYS.iter().copied().map(linux_key) {1050- handle1051- .set_keybit(key)1052- .context("enabling a bounded keyboard key capability")?;1053- }1054- handle1055- .create(1056- &InputId {1057- bustype: UINPUT_BUS_TYPE,1058- vendor: UINPUT_VENDOR_ID,1059- product: KEYBOARD_PRODUCT_ID,1060- version: UINPUT_DEVICE_VERSION,1061- },1062- KEYBOARD_DEVICE_NAME,1063- 0,1064- &[],1065- )1066- .context("registering Sigil virtual keyboard")?;1067-1068- Ok(Self {1069- handle,1070- pressed: BTreeSet::new(),1071- })1072- }1073-1074- fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {1075- match event {1076- ProtocolInputEvent::KeyDown { k } => {1077- self.press(linux_key(map_key(k).context("unsupported keyboard key")?))1078- }1079- ProtocolInputEvent::KeyUp { k } => {1080- self.release(linux_key(map_key(k).context("unsupported keyboard key")?))1081- }1082- ProtocolInputEvent::KeyClick { k } => {1083- self.click(linux_key(map_key(k).context("unsupported keyboard key")?))1084- }1085- _ => unreachable!("only keyboard events are routed to the keyboard device"),1086- }1087- }1088-1089- fn press(&mut self, key: Key) -> Result<()> {1090- self.emit(&[key_event(key, KeyState::PRESSED), synchronize_event()])?;1091- self.pressed.insert(key);1092- Ok(())1093- }1094-1095- fn release(&mut self, key: Key) -> Result<()> {1096- self.emit(&[key_event(key, KeyState::RELEASED), synchronize_event()])?;1097- self.pressed.remove(&key);1098- Ok(())1099- }1100-1101- fn click(&mut self, key: Key) -> Result<()> {1102- ensure!(1103- !self.pressed.contains(&key),1104- "click transition is invalid while the key is held"1105- );1106- self.emit(&[1107- key_event(key, KeyState::PRESSED),1108- synchronize_event(),1109- key_event(key, KeyState::RELEASED),1110- synchronize_event(),1111- ])1112- }1113-1114- fn release_all(&mut self) -> Result<()> {1115- if self.pressed.is_empty() {1116- return Ok(());1117- }1118- let mut events = Vec::with_capacity(self.pressed.len() + 1);1119- events.extend(1120- self.pressed1121- .iter()1122- .copied()1123- .map(|key| key_event(key, KeyState::RELEASED)),1124- );1125- events.push(synchronize_event());1126- ensure!(1127- events.len() <= MAX_KEYBOARD_EVENTS,1128- "keyboard release set exceeded its static bound"1129- );1130- self.emit(&events)?;1131- self.pressed.clear();1132- Ok(())1133- }1134-1135- fn emit(&self, events: &[input_linux::sys::input_event]) -> Result<()> {1136- ensure!(1137- !events.is_empty() && events.len() <= MAX_KEYBOARD_EVENTS,1138- "keyboard event batch is outside its static bound"1139- );1140- let written = self1141- .handle1142- .write(events)1143- .context("writing nonblocking keyboard event batch")?;1144- ensure!(1145- written == events.len(),1146- "uinput accepted only {written} of {} keyboard events",1147- events.len()1148- );1149- Ok(())1150- }1151- }1152-1153- impl Drop for KeyboardDevice {1154- fn drop(&mut self) {1155- let _ = self.release_all();1156- let _ = self.handle.dev_destroy();1157- }1158- }1159-1160- struct GamepadDevice {1161- handle: UInputHandle<File>,1162- }1163-1164- impl GamepadDevice {1165- fn open(config: &UinputConfig) -> Result<Self> {1166- let handle = open_validated_handle(config, "gamepad")?;1167-1168- handle1169- .set_evbit(EventKind::Key)1170- .context("enabling gamepad key events")?;1171- for (button, _) in map_gamepad_state(&GamepadState::default()).buttons {1172- handle1173- .set_keybit(linux_gamepad_button(button))1174- .context("enabling a bounded gamepad button capability")?;1175- }1176- handle1177- .set_evbit(EventKind::Absolute)1178- .context("enabling gamepad absolute-axis events")?;1179- for (axis, _) in map_gamepad_state(&GamepadState::default()).axes {1180- handle1181- .set_absbit(linux_gamepad_axis(axis))1182- .context("enabling a bounded gamepad axis capability")?;1183- }1184-1185- let axes = [1186- absolute_range(1187- AbsoluteAxis::X,1188- i32::from(GAMEPAD_AXIS_MIN),1189- i32::from(GAMEPAD_AXIS_MAX),1190- ),1191- absolute_range(1192- AbsoluteAxis::Y,1193- i32::from(GAMEPAD_AXIS_MIN),1194- i32::from(GAMEPAD_AXIS_MAX),1195- ),1196- absolute_range(1197- AbsoluteAxis::RX,1198- i32::from(GAMEPAD_AXIS_MIN),1199- i32::from(GAMEPAD_AXIS_MAX),1200- ),1201- absolute_range(1202- AbsoluteAxis::RY,1203- i32::from(GAMEPAD_AXIS_MIN),1204- i32::from(GAMEPAD_AXIS_MAX),1205- ),1206- absolute_range(AbsoluteAxis::Z, 0, i32::from(GAMEPAD_TRIGGER_MAX)),1207- absolute_range(AbsoluteAxis::RZ, 0, i32::from(GAMEPAD_TRIGGER_MAX)),1208- absolute_range(AbsoluteAxis::Hat0X, -1, 1),1209- absolute_range(AbsoluteAxis::Hat0Y, -1, 1),1210- ];1211- handle1212- .create(1213- &InputId {1214- bustype: UINPUT_BUS_TYPE,1215- vendor: UINPUT_VENDOR_ID,1216- product: GAMEPAD_PRODUCT_ID,1217- version: UINPUT_DEVICE_VERSION,1218- },1219- GAMEPAD_DEVICE_NAME,1220- 0,1221- &axes,1222- )1223- .context("registering Sigil virtual gamepad")?;1224- let device = Self { handle };1225- device.neutralize()?;1226- Ok(device)1227- }1228-1229- fn apply(&self, state: &GamepadState) -> Result<()> {1230- state.validate().context("validating gamepad snapshot")?;1231- let report = map_gamepad_state(state);1232- let mut events = [synchronize_event(); GAMEPAD_REPORT_EVENTS];1233- let mut index = 0;1234- for (button, pressed) in report.buttons {1235- events[index] = key_event(linux_gamepad_button(button), KeyState::pressed(pressed));1236- index += 1;1237- }1238- for (axis, value) in report.axes {1239- events[index] = absolute_event(linux_gamepad_axis(axis), value);1240- index += 1;1241- }1242- events[index] = synchronize_event();1243- index += 1;1244- ensure!(1245- index == GAMEPAD_REPORT_EVENTS,1246- "gamepad report violated its static event bound"1247- );1248- let written = self1249- .handle1250- .write(&events)1251- .context("writing nonblocking gamepad snapshot")?;1252- ensure!(1253- written == events.len(),1254- "uinput accepted only {written} of {} gamepad events",1255- events.len()1256- );1257- Ok(())1258- }1259-1260- fn neutralize(&self) -> Result<()> {1261- self.apply(&GamepadState::default())1262- .context("neutralizing virtual gamepad")1263- }1264- }1265-1266- impl Drop for GamepadDevice {1267- fn drop(&mut self) {1268- let _ = self.neutralize();1269- let _ = self.handle.dev_destroy();1270- }1271- }1272-1273- fn open_validated_handle(1274- config: &UinputConfig,1275- device_class: &str,1276- ) -> Result<UInputHandle<File>> {1277- let handle = UInputHandle::new(open_validated_device(config)?);1278- let version = handle1279- .version()1280- .with_context(|| format!("querying uinput interface version for {device_class}"))?;1281- ensure!(1282- version >= 5,1283- "uinput interface version {version} is too old for {device_class}"1284- );1285- Ok(handle)1286- }1287-1288- fn open_validated_device(config: &UinputConfig) -> Result<File> {1289- let mut options = OpenOptions::new();1290- options1291- .read(true)1292- .write(true)1293- .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK);1294- let file = options.open(&config.device_path).with_context(|| {1295- format!(1296- "opening configured uinput device {} without following symlinks",1297- config.device_path.display()1298- )1299- })?;1300- validate_open_device(&file, config)?;1301- Ok(file)1302- }1303-1304- fn validate_open_device(file: &File, config: &UinputConfig) -> Result<()> {1305- let metadata = file.metadata().with_context(|| {1306- format!(1307- "inspecting opened uinput device {}",1308- config.device_path.display()1309- )1310- })?;1311- ensure!(1312- metadata.file_type().is_char_device(),1313- "configured uinput path is not a character device"1314- );1315- ensure!(1316- metadata.uid() == config.expected_owner_uid,1317- "configured uinput owner UID changed"1318- );1319- ensure!(1320- metadata.gid() == config.expected_group_gid,1321- "configured uinput group GID changed"1322- );1323- ensure!(1324- metadata.mode() & 0o7777 == config.expected_mode,1325- "configured uinput permission mode changed"1326- );1327- let device = metadata.rdev();1328- ensure!(1329- libc::major(device) == UINPUT_MAJOR && libc::minor(device) == UINPUT_MINOR,1330- "configured character device is not the Linux uinput misc device"1331- );1332- validate_access_acl(file, config)?;1333- Ok(())1334- }1335-1336- fn validate_access_acl(file: &File, config: &UinputConfig) -> Result<()> {1337- if let Some(expected_uid) = config.expected_acl_user_uid {1338- let effective_uid = unsafe { libc::geteuid() };1339- ensure!(1340- expected_uid == effective_uid,1341- "configured uinput ACL user UID {expected_uid} does not match Sigil effective UID {effective_uid}"1342- );1343- }1344-1345- let attribute = c"system.posix_acl_access";1346- let mut bytes = [0_u8; POSIX_ACL_REQUIRED_BYTES];1347- let result = unsafe {1348- libc::fgetxattr(1349- file.as_raw_fd(),1350- attribute.as_ptr(),1351- bytes.as_mut_ptr().cast(),1352- bytes.len(),1353- )1354- };1355- if result >= 0 {1356- let Some(expected_uid) = config.expected_acl_user_uid else {1357- anyhow::bail!(1358- "configured uinput device must use the group-only scheme with no extended access ACL; set uinput.expected_acl_user_uid only for the explicit one-user ACL scheme"1359- );1360- };1361- let length = usize::try_from(result)1362- .context("converting configured uinput access ACL length")?;1363- return validate_single_user_access_acl(1364- bytes1365- .get(..length)1366- .context("configured uinput access ACL exceeded its fixed bound")?,1367- expected_uid,1368- config.expected_mode,1369- )1370- .context("validating configured uinput one-user access ACL");1371- }1372- let error = std::io::Error::last_os_error();1373- if error1374- .raw_os_error()1375- .is_some_and(|code| code == libc::ENODATA || code == libc::ENOTSUP)1376- {1377- ensure!(1378- config.expected_acl_user_uid.is_none(),1379- "configured uinput device is missing the explicitly required one-user access ACL"1380- );1381- return Ok(());1382- }1383- if error.raw_os_error() == Some(libc::ERANGE) {1384- if config.expected_acl_user_uid.is_some() {1385- anyhow::bail!("configured uinput access ACL exceeds the exact one-user ACL scheme");1386- }1387- anyhow::bail!(1388- "configured uinput device must use the group-only scheme with no extended access ACL"1389- );1390- }1391- Err(error).context("checking configured uinput device access ACL")1392- }1393-1394- fn absolute_range(axis: AbsoluteAxis, minimum: i32, maximum: i32) -> AbsoluteInfoSetup {1395- AbsoluteInfoSetup {1396- axis,1397- info: AbsoluteInfo {1398- minimum,1399- maximum,1400- ..AbsoluteInfo::default()1401- },1402- }1403- }1404-1405- fn mouse_button(button: u8) -> Option<Key> {1406- match button {1407- 1 => Some(Key::ButtonLeft),1408- 2 => Some(Key::ButtonRight),1409- 3 => Some(Key::ButtonMiddle),1410- _ => None,1411- }1412- }1413-1414- fn linux_gamepad_button(button: GamepadButton) -> Key {1415- match button {1416- GamepadButton::A => Key::ButtonSouth,1417- GamepadButton::B => Key::ButtonEast,1418- GamepadButton::X => Key::ButtonNorth,1419- GamepadButton::Y => Key::ButtonWest,1420- GamepadButton::LeftShoulder => Key::ButtonTL,1421- GamepadButton::RightShoulder => Key::ButtonTR,1422- GamepadButton::Back => Key::ButtonSelect,1423- GamepadButton::Start => Key::ButtonStart,1424- GamepadButton::Guide => Key::ButtonMode,1425- GamepadButton::LeftStick => Key::ButtonThumbl,1426- GamepadButton::RightStick => Key::ButtonThumbr,1427- }1428- }1429-1430- fn linux_gamepad_axis(axis: GamepadAxis) -> AbsoluteAxis {1431- match axis {1432- GamepadAxis::LeftX => AbsoluteAxis::X,1433- GamepadAxis::LeftY => AbsoluteAxis::Y,1434- GamepadAxis::RightX => AbsoluteAxis::RX,1435- GamepadAxis::RightY => AbsoluteAxis::RY,1436- GamepadAxis::LeftTrigger => AbsoluteAxis::Z,1437- GamepadAxis::RightTrigger => AbsoluteAxis::RZ,1438- GamepadAxis::DpadX => AbsoluteAxis::Hat0X,1439- GamepadAxis::DpadY => AbsoluteAxis::Hat0Y,1440- }1441- }1442-1443- fn key_event(key: Key, state: KeyState) -> input_linux::sys::input_event {1444- KeyEvent::new(EventTime::default(), key, state)1445- .into_event()1446- .into_raw()1447- }1448-1449- fn absolute_event(axis: AbsoluteAxis, value: i32) -> input_linux::sys::input_event {1450- AbsoluteEvent::new(EventTime::default(), axis, value)1451- .into_event()1452- .into_raw()1453- }1454-1455- fn relative_event(axis: RelativeAxis, value: i32) -> input_linux::sys::input_event {1456- RelativeEvent::new(EventTime::default(), axis, value)1457- .into_event()1458- .into_raw()1459- }1460-1461- fn synchronize_event() -> input_linux::sys::input_event {1462- SynchronizeEvent::report(EventTime::default())1463- .into_event()1464- .into_raw()1465- }1466-1467- pub(super) fn linux_key(key: MappedKey) -> Key {1468- match key {1469- MappedKey::A => Key::A,1470- MappedKey::B => Key::B,1471- MappedKey::C => Key::C,1472- MappedKey::D => Key::D,1473- MappedKey::E => Key::E,1474- MappedKey::F => Key::F,1475- MappedKey::G => Key::G,1476- MappedKey::H => Key::H,1477- MappedKey::I => Key::I,1478- MappedKey::J => Key::J,1479- MappedKey::K => Key::K,1480- MappedKey::L => Key::L,1481- MappedKey::M => Key::M,1482- MappedKey::N => Key::N,1483- MappedKey::O => Key::O,1484- MappedKey::P => Key::P,1485- MappedKey::Q => Key::Q,1486- MappedKey::R => Key::R,1487- MappedKey::S => Key::S,1488- MappedKey::T => Key::T,1489- MappedKey::U => Key::U,1490- MappedKey::V => Key::V,1491- MappedKey::W => Key::W,1492- MappedKey::X => Key::X,1493- MappedKey::Y => Key::Y,1494- MappedKey::Z => Key::Z,1495- MappedKey::Num0 => Key::Num0,1496- MappedKey::Num1 => Key::Num1,1497- MappedKey::Num2 => Key::Num2,1498- MappedKey::Num3 => Key::Num3,1499- MappedKey::Num4 => Key::Num4,1500- MappedKey::Num5 => Key::Num5,1501- MappedKey::Num6 => Key::Num6,1502- MappedKey::Num7 => Key::Num7,1503- MappedKey::Num8 => Key::Num8,1504- MappedKey::Num9 => Key::Num9,1505- MappedKey::Minus => Key::Minus,1506- MappedKey::Equal => Key::Equal,1507- MappedKey::LeftBrace => Key::LeftBrace,1508- MappedKey::RightBrace => Key::RightBrace,1509- MappedKey::Backslash => Key::Backslash,1510- MappedKey::Semicolon => Key::Semicolon,1511- MappedKey::Apostrophe => Key::Apostrophe,1512- MappedKey::Grave => Key::Grave,1513- MappedKey::Comma => Key::Comma,1514- MappedKey::Dot => Key::Dot,1515- MappedKey::Slash => Key::Slash,1516- MappedKey::NonUsBackslash => Key::NonUsBackslashAndPipe,1517- MappedKey::Ro => Key::Ro,1518- MappedKey::Yen => Key::Yen,1519- MappedKey::Enter => Key::Enter,1520- MappedKey::Tab => Key::Tab,1521- MappedKey::Space => Key::Space,1522- MappedKey::Backspace => Key::Backspace,1523- MappedKey::Escape => Key::Esc,1524- MappedKey::LeftShift => Key::LeftShift,1525- MappedKey::RightShift => Key::RightShift,1526- MappedKey::LeftCtrl => Key::LeftCtrl,1527- MappedKey::RightCtrl => Key::RightCtrl,1528- MappedKey::LeftAlt => Key::LeftAlt,1529- MappedKey::RightAlt => Key::RightAlt,1530- MappedKey::LeftMeta => Key::LeftMeta,1531- MappedKey::RightMeta => Key::RightMeta,1532- MappedKey::Up => Key::Up,1533- MappedKey::Down => Key::Down,1534- MappedKey::Left => Key::Left,1535- MappedKey::Right => Key::Right,1536- MappedKey::Home => Key::Home,1537- MappedKey::End => Key::End,1538- MappedKey::PageUp => Key::PageUp,1539- MappedKey::PageDown => Key::PageDown,1540- MappedKey::Insert => Key::Insert,1541- MappedKey::Delete => Key::Delete,1542- MappedKey::PrintScreen => Key::Sysrq,1543- MappedKey::F1 => Key::F1,1544- MappedKey::F2 => Key::F2,1545- MappedKey::F3 => Key::F3,1546- MappedKey::F4 => Key::F4,1547- MappedKey::F5 => Key::F5,1548- MappedKey::F6 => Key::F6,1549- MappedKey::F7 => Key::F7,1550- MappedKey::F8 => Key::F8,1551- MappedKey::F9 => Key::F9,1552- MappedKey::F10 => Key::F10,1553- MappedKey::F11 => Key::F11,1554- MappedKey::F12 => Key::F12,1555- }1556- }1557-}1558-1559-1560-mod tests {1561- use std::panic::{AssertUnwindSafe, catch_unwind};1562- use std::sync::Arc;1563-1564- use super::*;1565-1566- fn acl_entry(tag: u16, permissions: u16, id: u32) -> [u8; POSIX_ACL_ENTRY_BYTES] {1567- let mut bytes = [0_u8; POSIX_ACL_ENTRY_BYTES];1568- bytes[0..2].copy_from_slice(&tag.to_le_bytes());1569- bytes[2..4].copy_from_slice(&permissions.to_le_bytes());1570- bytes[4..8].copy_from_slice(&id.to_le_bytes());1571- bytes1572- }1573-1574- fn one_user_access_acl(uid: u32, mode: u32) -> Vec<u8> {1575- let entries = [1576- acl_entry(1577- POSIX_ACL_USER_OBJ,1578- ((mode >> 6) & 0o7) as u16,1579- POSIX_ACL_UNDEFINED_ID,1580- ),1581- acl_entry(POSIX_ACL_USER, POSIX_ACL_READ_WRITE, uid),1582- acl_entry(1583- POSIX_ACL_GROUP_OBJ,1584- ((mode >> 3) & 0o7) as u16,1585- POSIX_ACL_UNDEFINED_ID,1586- ),1587- acl_entry(1588- POSIX_ACL_MASK,1589- ((mode >> 3) & 0o7) as u16,1590- POSIX_ACL_UNDEFINED_ID,1591- ),1592- acl_entry(POSIX_ACL_OTHER, (mode & 0o7) as u16, POSIX_ACL_UNDEFINED_ID),1593- ];1594- let mut bytes = Vec::with_capacity(POSIX_ACL_REQUIRED_BYTES);1595- bytes.extend_from_slice(&POSIX_ACL_XATTR_VERSION.to_le_bytes());1596- for entry in entries {1597- bytes.extend_from_slice(&entry);1598- }1599- bytes1600- }1601-1602- 1603- fn exact_one_user_access_acl_is_accepted() {1604- let bytes = one_user_access_acl(1000, 0o660);1605- assert_eq!(bytes.len(), POSIX_ACL_REQUIRED_BYTES);1606- validate_single_user_access_acl(&bytes, 1000, 0o660).unwrap();1607- }1608-1609- 1610- fn one_user_access_acl_rejects_wrong_principal_and_permissions() {1611- let bytes = one_user_access_acl(1000, 0o660);1612- assert!(validate_single_user_access_acl(&bytes, 1001, 0o660).is_err());1613-1614- for permission_offset in [6, 14, 22, 30, 38] {1615- let mut changed = bytes.clone();1616- changed[permission_offset] ^= 0x01;1617- assert!(validate_single_user_access_acl(&changed, 1000, 0o660).is_err());1618- }1619-1620- let mut invalid_permissions = bytes;1621- invalid_permissions[14] = 0x08;1622- assert!(validate_single_user_access_acl(&invalid_permissions, 1000, 0o660).is_err());1623- }1624-1625- 1626- fn one_user_access_acl_rejects_extra_duplicate_and_named_group_entries() {1627- let bytes = one_user_access_acl(1000, 0o660);1628-1629- let mut extra = bytes.clone();1630- extra.extend_from_slice(&acl_entry(POSIX_ACL_USER, POSIX_ACL_READ_WRITE, 1001));1631- assert!(validate_single_user_access_acl(&extra, 1000, 0o660).is_err());1632-1633- let mut duplicate_user = bytes.clone();1634- duplicate_user[20..22].copy_from_slice(&POSIX_ACL_USER.to_le_bytes());1635- assert!(validate_single_user_access_acl(&duplicate_user, 1000, 0o660).is_err());1636-1637- let mut named_group = bytes;1638- named_group[20..22].copy_from_slice(&0x08_u16.to_le_bytes());1639- assert!(validate_single_user_access_acl(&named_group, 1000, 0o660).is_err());1640- }1641-1642- 1643- fn one_user_access_acl_rejects_bad_version_and_non_exact_lengths() {1644- let bytes = one_user_access_acl(1000, 0o660);1645-1646- let mut bad_version = bytes.clone();1647- bad_version[0..4].copy_from_slice(&3_u32.to_le_bytes());1648- assert!(validate_single_user_access_acl(&bad_version, 1000, 0o660).is_err());1649- assert!(validate_single_user_access_acl(&bytes[..bytes.len() - 1], 1000, 0o660).is_err());1650-1651- let mut oversized = bytes;1652- oversized.push(0);1653- assert!(validate_single_user_access_acl(&oversized, 1000, 0o660).is_err());1654- }1655-1656- 1657- fn virtual_input_topology_has_stable_distinct_identities() {1658- assert_eq!(UINPUT_BUS_TYPE, 0x06);1659- 1660- assert_eq!(UINPUT_BUS_TYPE, input_linux::sys::BUS_VIRTUAL);1661- assert_eq!(UINPUT_VENDOR_ID, 0x5347);1662- assert_eq!(UINPUT_DEVICE_VERSION, 1);1663- assert_eq!(POINTER_DEVICE_NAME, b"Sigil Spark Virtual Pointer");1664- assert_eq!(KEYBOARD_DEVICE_NAME, b"Sigil Spark Virtual Keyboard");1665- assert_eq!(GAMEPAD_DEVICE_NAME, b"Sigil Spark Virtual Gamepad");1666- assert_eq!(POINTER_PRODUCT_ID, 1);1667- assert_eq!(GAMEPAD_PRODUCT_ID, 2);1668- assert_eq!(KEYBOARD_PRODUCT_ID, 3);1669- assert_ne!(POINTER_PRODUCT_ID, KEYBOARD_PRODUCT_ID);1670- assert_ne!(POINTER_PRODUCT_ID, GAMEPAD_PRODUCT_ID);1671- assert_ne!(KEYBOARD_PRODUCT_ID, GAMEPAD_PRODUCT_ID);1672- }1673-1674- 1675- fn session_reset_recovers_a_lock_poisoned_while_applying_input() {1676- 1677- struct DeviceState {1678- released: bool,1679- }1680-1681- let device = Arc::new(Mutex::new(DeviceState::default()));1682- let panic_device = Arc::clone(&device);1683- let panic_result = catch_unwind(AssertUnwindSafe(move || {1684- let _device = panic_device.lock().unwrap();1685- panic!("simulate panic while applying uinput");1686- }));1687- assert!(panic_result.is_err());1688- assert!(device.lock().is_err());1689-1690- reset_through_poisoned_lock(device.as_ref(), |device| {1691- device.released = true;1692- Ok(())1693- })1694- .unwrap();1695-1696- let device = device.lock().unwrap_err().into_inner();1697- assert!(device.released);1698- }1699-1700- 1701- fn protocol_events_route_to_disjoint_virtual_devices() {1702- for event in [1703- InputEvent::MouseMove { x: 1, y: 2 },1704- InputEvent::MouseMoveRelative { dx: 1, dy: 2 },1705- InputEvent::MousePositionSync { x: 10, y: 20 },1706- InputEvent::MouseClick { b: 1 },1707- InputEvent::MouseDown { b: 2 },1708- InputEvent::MouseUp { b: 3 },1709- InputEvent::MouseScroll { dx: 1, dy: -1 },1710- ] {1711- assert_eq!(input_device_class(&event), Some(InputDeviceClass::Pointer));1712- }1713- for event in [1714- InputEvent::KeyDown { k: "a".into() },1715- InputEvent::KeyUp { k: "a".into() },1716- InputEvent::KeyClick { k: "a".into() },1717- ] {1718- assert_eq!(input_device_class(&event), Some(InputDeviceClass::Keyboard));1719- }1720- assert_eq!(1721- input_device_class(&InputEvent::Gamepad {1722- state: GamepadState::default(),1723- }),1724- Some(InputDeviceClass::Gamepad)1725- );1726- assert_eq!(1727- input_device_class(&InputEvent::Text {1728- s: "ignored".into()1729- }),1730- None1731- );1732- assert_eq!(input_device_class(&InputEvent::Probe), None);1733- }1734-1735- 1736- fn content_free_probe_requires_ack_and_never_reaches_uinput() {1737- assert!(ensure_event_was_negotiated(&InputMode::Uinput, &InputEvent::Probe, &[]).is_err());1738- assert!(1739- ensure_event_was_negotiated(1740- &InputMode::Uinput,1741- &InputEvent::Probe,1742- &[Capability::InputAck]1743- )1744- .is_ok()1745- );1746- }1747-1748- 1749- fn maps_client_named_keys_without_guessing() {1750- assert_eq!(map_key("Enter"), Some(MappedKey::Enter));1751- assert_eq!(map_key("Control"), Some(MappedKey::LeftCtrl));1752- assert_eq!(map_key("PageDown"), Some(MappedKey::PageDown));1753- assert_eq!(map_key("F13"), None);1754- assert_eq!(map_key("CapsLock"), None);1755- assert_eq!(map_key("Numpad1"), None);1756- assert_eq!(map_key(""), None);1757- }1758-1759- 1760- fn maps_bounded_browser_physical_keys_for_games_and_layouts() {1761- for (value, expected) in [1762- ("KeyQ", MappedKey::Q),1763- ("Digit2", MappedKey::Num2),1764- ("BracketLeft", MappedKey::LeftBrace),1765- ("IntlBackslash", MappedKey::NonUsBackslash),1766- ("IntlRo", MappedKey::Ro),1767- ("IntlYen", MappedKey::Yen),1768- ("ShiftLeft", MappedKey::LeftShift),1769- ("ShiftRight", MappedKey::RightShift),1770- ("ControlLeft", MappedKey::LeftCtrl),1771- ("ControlRight", MappedKey::RightCtrl),1772- ("AltLeft", MappedKey::LeftAlt),1773- ("AltRight", MappedKey::RightAlt),1774- ("MetaLeft", MappedKey::LeftMeta),1775- ("MetaRight", MappedKey::RightMeta),1776- ("ArrowUp", MappedKey::Up),1777- ("Insert", MappedKey::Insert),1778- ("PrintScreen", MappedKey::PrintScreen),1779- ("F1", MappedKey::F1),1780- ("F12", MappedKey::F12),1781- ] {1782- assert_eq!(map_key(value), Some(expected), "{value}");1783- assert!(ALL_MAPPED_KEYS.contains(&expected), "{value}");1784- }1785- let unique = ALL_MAPPED_KEYS1786- .iter()1787- .copied()1788- .collect::<std::collections::BTreeSet<_>>();1789- assert_eq!(unique.len(), ALL_MAPPED_KEYS.len());1790- }1791-1792- 1793- 1794- fn extended_physical_keys_translate_to_exact_linux_evdev_codes() {1795- for (mapped, expected) in [1796- (MappedKey::F1, input_linux::Key::F1),1797- (MappedKey::F12, input_linux::Key::F12),1798- (MappedKey::Insert, input_linux::Key::Insert),1799- (MappedKey::PrintScreen, input_linux::Key::Sysrq),1800- (MappedKey::RightShift, input_linux::Key::RightShift),1801- (MappedKey::RightCtrl, input_linux::Key::RightCtrl),1802- (MappedKey::RightAlt, input_linux::Key::RightAlt),1803- (MappedKey::RightMeta, input_linux::Key::RightMeta),1804- (1805- MappedKey::NonUsBackslash,1806- input_linux::Key::NonUsBackslashAndPipe,1807- ),1808- (MappedKey::Ro, input_linux::Key::Ro),1809- (MappedKey::Yen, input_linux::Key::Yen),1810- ] {1811- assert_eq!(linux::linux_key(mapped), expected);1812- }1813- }1814-1815- 1816- fn maps_us_printable_keys_to_physical_keys() {1817- assert_eq!(map_key("a"), Some(MappedKey::A));1818- assert_eq!(map_key("A"), Some(MappedKey::A));1819- assert_eq!(map_key("!"), Some(MappedKey::Num1));1820- assert_eq!(map_key("?"), Some(MappedKey::Slash));1821- assert_eq!(map_key("Ă©"), None);1822- assert_eq!(map_key("ab"), None);1823- }1824-1825- 1826- fn relative_pointer_events_require_relative_pointer_negotiation() {1827- let movement = InputEvent::MouseMoveRelative { dx: 4, dy: -3 };1828- let synchronization = InputEvent::MousePositionSync { x: 123, y: 456 };1829- let click = InputEvent::MouseClick { b: 1 };1830- let legacy = InputEvent::MouseMove { x: 10, y: 20 };1831- for event in [&movement, &synchronization, &click] {1832- assert!(ensure_event_was_negotiated(&InputMode::Uinput, event, &[]).is_err());1833- assert!(1834- ensure_event_was_negotiated(1835- &InputMode::Uinput,1836- event,1837- &[Capability::AbsolutePointer]1838- )1839- .is_err()1840- );1841- assert!(1842- ensure_event_was_negotiated(1843- &InputMode::Uinput,1844- event,1845- &[Capability::RelativePointer]1846- )1847- .is_ok()1848- );1849- }1850- assert!(1851- ensure_event_was_negotiated(1852- &InputMode::Uinput,1853- &legacy,1854- &[Capability::RelativePointer]1855- )1856- .is_err()1857- );1858- }1859-1860- 1861- fn pointer_position_sync_uses_two_ordered_relative_reports() {1862- assert_eq!(1863- pointer_position_sync_report(1_280, 800),1864- [1865- PointerReportEvent::RelativeX(-32_767),1866- PointerReportEvent::RelativeY(-32_767),1867- PointerReportEvent::Synchronize,1868- PointerReportEvent::RelativeX(1_280),1869- PointerReportEvent::RelativeY(800),1870- PointerReportEvent::Synchronize,1871- ]1872- );1873- }1874-1875- 1876- fn maps_every_gamepad_control_into_one_bounded_report() {1877- let state = GamepadState {1878- a: true,1879- b: true,1880- x: true,1881- y: true,1882- left_shoulder: true,1883- right_shoulder: true,1884- back: true,1885- start: true,1886- guide: true,1887- left_stick: true,1888- right_stick: true,1889- dpad_up: true,1890- dpad_right: true,1891- left_x: -32_767,1892- left_y: 32_767,1893- right_x: -123,1894- right_y: 456,1895- left_trigger: 12_345,1896- right_trigger: 32_767,1897- ..GamepadState::default()1898- };1899- let report = map_gamepad_state(&state);1900- assert_eq!(report.buttons.len(), 11);1901- assert!(report.buttons.into_iter().all(|(_, pressed)| pressed));1902- assert_eq!(1903- report.axes,1904- [1905- (GamepadAxis::LeftX, -32_767),1906- (GamepadAxis::LeftY, 32_767),1907- (GamepadAxis::RightX, -123),1908- (GamepadAxis::RightY, 456),1909- (GamepadAxis::LeftTrigger, 12_345),1910- (GamepadAxis::RightTrigger, 32_767),1911- (GamepadAxis::DpadX, 1),1912- (GamepadAxis::DpadY, -1),1913- ]1914- );1915- }1916-1917- 1918- fn neutral_gamepad_report_releases_buttons_and_axes() {1919- let report = map_gamepad_state(&GamepadState::default());1920- assert!(report.buttons.into_iter().all(|(_, pressed)| !pressed));1921- assert!(report.axes.into_iter().all(|(_, value)| value == 0));1922- }1923-1924- 1925- fn uinput_rejects_unnegotiated_gamepad_snapshots() {1926- let event = InputEvent::Gamepad {1927- state: GamepadState::default(),1928- };1929- assert!(ensure_event_was_negotiated(&InputMode::Uinput, &event, &[]).is_err());1930- assert!(1931- ensure_event_was_negotiated(&InputMode::Uinput, &event, &[Capability::Gamepad]).is_ok()1932- );1933- assert!(ensure_event_was_negotiated(&InputMode::Log, &event, &[]).is_ok());1934- }1935-1936- 1937- fn advertised_capabilities_follow_operational_mode() {1938- let disabled = InputBackend {1939- mode: InputMode::Disabled,1940- 1941- device: None,1942- };1943- assert_eq!(disabled.capabilities(), &[Capability::InputAck]);1944-1945- let log = InputBackend {1946- mode: InputMode::Log,1947- 1948- device: None,1949- };1950- assert_eq!(log.capabilities(), &[Capability::InputAck]);1951- assert!(!log.capabilities().contains(&Capability::Keyboard));1952- assert_eq!(1953- capabilities_for_mode(&InputMode::Uinput),1954- &[1955- Capability::RelativePointer,1956- Capability::Keyboard,1957- Capability::Gamepad,1958- Capability::InputAck,1959- ]1960- );1961- assert!(!capabilities_for_mode(&InputMode::Uinput).contains(&Capability::AbsolutePointer));1962- }1963-}@@ -0,0 +1,193 @@
1+2+use std::mem::size_of;3+4+5+use anyhow::{Result, bail};6+7+8+const POSIX_ACL_XATTR_VERSION: u32 = 0x0002;9+10+const POSIX_ACL_ENTRY_BYTES: usize = 8;11+12+const POSIX_ACL_REQUIRED_ENTRIES: usize = 5;13+14+pub(crate) const POSIX_ACL_REQUIRED_BYTES: usize =15+ size_of::<u32>() + POSIX_ACL_ENTRY_BYTES * POSIX_ACL_REQUIRED_ENTRIES;16+17+const POSIX_ACL_UNDEFINED_ID: u32 = u32::MAX;18+19+const POSIX_ACL_USER_OBJ: u16 = 0x01;20+21+const POSIX_ACL_USER: u16 = 0x02;22+23+const POSIX_ACL_GROUP_OBJ: u16 = 0x04;24+25+const POSIX_ACL_MASK: u16 = 0x10;26+27+const POSIX_ACL_OTHER: u16 = 0x20;28+29+const POSIX_ACL_READ_WRITE: u16 = 0x06;30+31+/// Validate the only extended uinput ACL shape Sigil accepts.32+///33+/// Linux stores POSIX ACL xattrs as one little-endian version followed by34+/// fixed-size entries. Requiring the canonical five-entry form prevents a35+/// configured one-user exception from admitting extra users or groups.36+37+pub(crate) fn validate_single_user_access_acl(38+ bytes: &[u8],39+ expected_uid: u32,40+ expected_mode: u32,41+) -> Result<()> {42+ if bytes.len() != POSIX_ACL_REQUIRED_BYTES {43+ bail!(44+ "configured uinput access ACL must contain exactly one named user and no named groups"45+ );46+ }47+ let version = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);48+ if version != POSIX_ACL_XATTR_VERSION {49+ bail!("configured uinput access ACL has an unsupported Linux ACL version");50+ }51+52+ let expected_entries = [53+ (54+ POSIX_ACL_USER_OBJ,55+ ((expected_mode >> 6) & 0o7) as u16,56+ POSIX_ACL_UNDEFINED_ID,57+ ),58+ (POSIX_ACL_USER, POSIX_ACL_READ_WRITE, expected_uid),59+ (60+ POSIX_ACL_GROUP_OBJ,61+ ((expected_mode >> 3) & 0o7) as u16,62+ POSIX_ACL_UNDEFINED_ID,63+ ),64+ (65+ POSIX_ACL_MASK,66+ ((expected_mode >> 3) & 0o7) as u16,67+ POSIX_ACL_UNDEFINED_ID,68+ ),69+ (70+ POSIX_ACL_OTHER,71+ (expected_mode & 0o7) as u16,72+ POSIX_ACL_UNDEFINED_ID,73+ ),74+ ];75+76+ for (index, (expected_tag, expected_permissions, expected_id)) in77+ expected_entries.into_iter().enumerate()78+ {79+ let offset = size_of::<u32>() + index * POSIX_ACL_ENTRY_BYTES;80+ let tag = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);81+ let permissions = u16::from_le_bytes([bytes[offset + 2], bytes[offset + 3]]);82+ let id = u32::from_le_bytes([83+ bytes[offset + 4],84+ bytes[offset + 5],85+ bytes[offset + 6],86+ bytes[offset + 7],87+ ]);88+ if permissions & !0o7 != 0 {89+ bail!("configured uinput access ACL contains invalid permission bits");90+ }91+ if tag != expected_tag || permissions != expected_permissions || id != expected_id {92+ bail!(93+ "configured uinput access ACL does not match the exact one-user owner/group/mode contract"94+ );95+ }96+ }97+ Ok(())98+}99+100+101+mod tests {102+ use super::*;103+104+ fn acl_entry(tag: u16, permissions: u16, id: u32) -> [u8; POSIX_ACL_ENTRY_BYTES] {105+ let mut bytes = [0_u8; POSIX_ACL_ENTRY_BYTES];106+ bytes[0..2].copy_from_slice(&tag.to_le_bytes());107+ bytes[2..4].copy_from_slice(&permissions.to_le_bytes());108+ bytes[4..8].copy_from_slice(&id.to_le_bytes());109+ bytes110+ }111+112+ fn one_user_access_acl(uid: u32, mode: u32) -> Vec<u8> {113+ let entries = [114+ acl_entry(115+ POSIX_ACL_USER_OBJ,116+ ((mode >> 6) & 0o7) as u16,117+ POSIX_ACL_UNDEFINED_ID,118+ ),119+ acl_entry(POSIX_ACL_USER, POSIX_ACL_READ_WRITE, uid),120+ acl_entry(121+ POSIX_ACL_GROUP_OBJ,122+ ((mode >> 3) & 0o7) as u16,123+ POSIX_ACL_UNDEFINED_ID,124+ ),125+ acl_entry(126+ POSIX_ACL_MASK,127+ ((mode >> 3) & 0o7) as u16,128+ POSIX_ACL_UNDEFINED_ID,129+ ),130+ acl_entry(POSIX_ACL_OTHER, (mode & 0o7) as u16, POSIX_ACL_UNDEFINED_ID),131+ ];132+ let mut bytes = Vec::with_capacity(POSIX_ACL_REQUIRED_BYTES);133+ bytes.extend_from_slice(&POSIX_ACL_XATTR_VERSION.to_le_bytes());134+ for entry in entries {135+ bytes.extend_from_slice(&entry);136+ }137+ bytes138+ }139+140+ 141+ fn exact_one_user_access_acl_is_accepted() {142+ let bytes = one_user_access_acl(1000, 0o660);143+ assert_eq!(bytes.len(), POSIX_ACL_REQUIRED_BYTES);144+ validate_single_user_access_acl(&bytes, 1000, 0o660).unwrap();145+ }146+147+ 148+ fn one_user_access_acl_rejects_wrong_principal_and_permissions() {149+ let bytes = one_user_access_acl(1000, 0o660);150+ assert!(validate_single_user_access_acl(&bytes, 1001, 0o660).is_err());151+152+ for permission_offset in [6, 14, 22, 30, 38] {153+ let mut changed = bytes.clone();154+ changed[permission_offset] ^= 0x01;155+ assert!(validate_single_user_access_acl(&changed, 1000, 0o660).is_err());156+ }157+158+ let mut invalid_permissions = bytes;159+ invalid_permissions[14] = 0x08;160+ assert!(validate_single_user_access_acl(&invalid_permissions, 1000, 0o660).is_err());161+ }162+163+ 164+ fn one_user_access_acl_rejects_extra_duplicate_and_named_group_entries() {165+ let bytes = one_user_access_acl(1000, 0o660);166+167+ let mut extra = bytes.clone();168+ extra.extend_from_slice(&acl_entry(POSIX_ACL_USER, POSIX_ACL_READ_WRITE, 1001));169+ assert!(validate_single_user_access_acl(&extra, 1000, 0o660).is_err());170+171+ let mut duplicate_user = bytes.clone();172+ duplicate_user[20..22].copy_from_slice(&POSIX_ACL_USER.to_le_bytes());173+ assert!(validate_single_user_access_acl(&duplicate_user, 1000, 0o660).is_err());174+175+ let mut named_group = bytes;176+ named_group[20..22].copy_from_slice(&0x08_u16.to_le_bytes());177+ assert!(validate_single_user_access_acl(&named_group, 1000, 0o660).is_err());178+ }179+180+ 181+ fn one_user_access_acl_rejects_bad_version_and_non_exact_lengths() {182+ let bytes = one_user_access_acl(1000, 0o660);183+184+ let mut bad_version = bytes.clone();185+ bad_version[0..4].copy_from_slice(&3_u32.to_le_bytes());186+ assert!(validate_single_user_access_acl(&bad_version, 1000, 0o660).is_err());187+ assert!(validate_single_user_access_acl(&bytes[..bytes.len() - 1], 1000, 0o660).is_err());188+189+ let mut oversized = bytes;190+ oversized.push(0);191+ assert!(validate_single_user_access_acl(&oversized, 1000, 0o660).is_err());192+ }193+}@@ -0,0 +1,745 @@
1+2+use input_linux::Key;3+4+use sigil_protocol::GamepadState;5+6+use sigil_protocol::InputEvent;7+8+9+10+pub(crate) enum InputDeviceClass {11+ Pointer,12+ Keyboard,13+ Gamepad,14+}15+16+17+18+pub(crate) enum PointerReportEvent {19+ RelativeX(i32),20+ RelativeY(i32),21+ Synchronize,22+}23+24+25+/// Emulate an absolute position on the relative-only appliance pointer.26+///27+/// This is correct only when Gamescope clamps the first report at the surface28+/// origin and applies neither acceleration nor scaling to either report. The29+/// hardware UAT verifies both the synchronized position and one subsequent30+/// relative delta through the Xwayland pointer tracker.31+pub(crate) fn pointer_position_sync_report(x: i32, y: i32) -> [PointerReportEvent; 6] {32+ [33+ PointerReportEvent::RelativeX(sigil_protocol::RELATIVE_POINTER_DELTA_MIN),34+ PointerReportEvent::RelativeY(sigil_protocol::RELATIVE_POINTER_DELTA_MIN),35+ PointerReportEvent::Synchronize,36+ PointerReportEvent::RelativeX(x),37+ PointerReportEvent::RelativeY(y),38+ PointerReportEvent::Synchronize,39+ ]40+}41+42+43+pub(crate) fn input_device_class(event: &InputEvent) -> Option<InputDeviceClass> {44+ match event {45+ InputEvent::Probe => None,46+ InputEvent::MouseMove { .. }47+ | InputEvent::MouseMoveRelative { .. }48+ | InputEvent::MousePositionSync { .. }49+ | InputEvent::MouseClick { .. }50+ | InputEvent::MouseDown { .. }51+ | InputEvent::MouseUp { .. }52+ | InputEvent::MouseScroll { .. } => Some(InputDeviceClass::Pointer),53+ InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } | InputEvent::KeyClick { .. } => {54+ Some(InputDeviceClass::Keyboard)55+ }56+ InputEvent::Gamepad { .. } => Some(InputDeviceClass::Gamepad),57+ InputEvent::Text { .. } => None,58+ }59+}60+61+62+63+pub(crate) enum MappedKey {64+ A,65+ B,66+ C,67+ D,68+ E,69+ F,70+ G,71+ H,72+ I,73+ J,74+ K,75+ L,76+ M,77+ N,78+ O,79+ P,80+ Q,81+ R,82+ S,83+ T,84+ U,85+ V,86+ W,87+ X,88+ Y,89+ Z,90+ Num0,91+ Num1,92+ Num2,93+ Num3,94+ Num4,95+ Num5,96+ Num6,97+ Num7,98+ Num8,99+ Num9,100+ Minus,101+ Equal,102+ LeftBrace,103+ RightBrace,104+ Backslash,105+ Semicolon,106+ Apostrophe,107+ Grave,108+ Comma,109+ Dot,110+ Slash,111+ NonUsBackslash,112+ Ro,113+ Yen,114+ Enter,115+ Tab,116+ Space,117+ Backspace,118+ Escape,119+ LeftShift,120+ RightShift,121+ LeftCtrl,122+ RightCtrl,123+ LeftAlt,124+ RightAlt,125+ LeftMeta,126+ RightMeta,127+ Up,128+ Down,129+ Left,130+ Right,131+ Home,132+ End,133+ PageUp,134+ PageDown,135+ Insert,136+ Delete,137+ PrintScreen,138+ F1,139+ F2,140+ F3,141+ F4,142+ F5,143+ F6,144+ F7,145+ F8,146+ F9,147+ F10,148+ F11,149+ F12,150+}151+152+153+pub(crate) const ALL_MAPPED_KEYS: &[MappedKey] = &[154+ MappedKey::A,155+ MappedKey::B,156+ MappedKey::C,157+ MappedKey::D,158+ MappedKey::E,159+ MappedKey::F,160+ MappedKey::G,161+ MappedKey::H,162+ MappedKey::I,163+ MappedKey::J,164+ MappedKey::K,165+ MappedKey::L,166+ MappedKey::M,167+ MappedKey::N,168+ MappedKey::O,169+ MappedKey::P,170+ MappedKey::Q,171+ MappedKey::R,172+ MappedKey::S,173+ MappedKey::T,174+ MappedKey::U,175+ MappedKey::V,176+ MappedKey::W,177+ MappedKey::X,178+ MappedKey::Y,179+ MappedKey::Z,180+ MappedKey::Num0,181+ MappedKey::Num1,182+ MappedKey::Num2,183+ MappedKey::Num3,184+ MappedKey::Num4,185+ MappedKey::Num5,186+ MappedKey::Num6,187+ MappedKey::Num7,188+ MappedKey::Num8,189+ MappedKey::Num9,190+ MappedKey::Minus,191+ MappedKey::Equal,192+ MappedKey::LeftBrace,193+ MappedKey::RightBrace,194+ MappedKey::Backslash,195+ MappedKey::Semicolon,196+ MappedKey::Apostrophe,197+ MappedKey::Grave,198+ MappedKey::Comma,199+ MappedKey::Dot,200+ MappedKey::Slash,201+ MappedKey::NonUsBackslash,202+ MappedKey::Ro,203+ MappedKey::Yen,204+ MappedKey::Enter,205+ MappedKey::Tab,206+ MappedKey::Space,207+ MappedKey::Backspace,208+ MappedKey::Escape,209+ MappedKey::LeftShift,210+ MappedKey::RightShift,211+ MappedKey::LeftCtrl,212+ MappedKey::RightCtrl,213+ MappedKey::LeftAlt,214+ MappedKey::RightAlt,215+ MappedKey::LeftMeta,216+ MappedKey::RightMeta,217+ MappedKey::Up,218+ MappedKey::Down,219+ MappedKey::Left,220+ MappedKey::Right,221+ MappedKey::Home,222+ MappedKey::End,223+ MappedKey::PageUp,224+ MappedKey::PageDown,225+ MappedKey::Insert,226+ MappedKey::Delete,227+ MappedKey::PrintScreen,228+ MappedKey::F1,229+ MappedKey::F2,230+ MappedKey::F3,231+ MappedKey::F4,232+ MappedKey::F5,233+ MappedKey::F6,234+ MappedKey::F7,235+ MappedKey::F8,236+ MappedKey::F9,237+ MappedKey::F10,238+ MappedKey::F11,239+ MappedKey::F12,240+];241+242+243+pub(crate) fn map_key(value: &str) -> Option<MappedKey> {244+ match value {245+ "KeyA" => Some(MappedKey::A),246+ "KeyB" => Some(MappedKey::B),247+ "KeyC" => Some(MappedKey::C),248+ "KeyD" => Some(MappedKey::D),249+ "KeyE" => Some(MappedKey::E),250+ "KeyF" => Some(MappedKey::F),251+ "KeyG" => Some(MappedKey::G),252+ "KeyH" => Some(MappedKey::H),253+ "KeyI" => Some(MappedKey::I),254+ "KeyJ" => Some(MappedKey::J),255+ "KeyK" => Some(MappedKey::K),256+ "KeyL" => Some(MappedKey::L),257+ "KeyM" => Some(MappedKey::M),258+ "KeyN" => Some(MappedKey::N),259+ "KeyO" => Some(MappedKey::O),260+ "KeyP" => Some(MappedKey::P),261+ "KeyQ" => Some(MappedKey::Q),262+ "KeyR" => Some(MappedKey::R),263+ "KeyS" => Some(MappedKey::S),264+ "KeyT" => Some(MappedKey::T),265+ "KeyU" => Some(MappedKey::U),266+ "KeyV" => Some(MappedKey::V),267+ "KeyW" => Some(MappedKey::W),268+ "KeyX" => Some(MappedKey::X),269+ "KeyY" => Some(MappedKey::Y),270+ "KeyZ" => Some(MappedKey::Z),271+ "Digit0" => Some(MappedKey::Num0),272+ "Digit1" => Some(MappedKey::Num1),273+ "Digit2" => Some(MappedKey::Num2),274+ "Digit3" => Some(MappedKey::Num3),275+ "Digit4" => Some(MappedKey::Num4),276+ "Digit5" => Some(MappedKey::Num5),277+ "Digit6" => Some(MappedKey::Num6),278+ "Digit7" => Some(MappedKey::Num7),279+ "Digit8" => Some(MappedKey::Num8),280+ "Digit9" => Some(MappedKey::Num9),281+ "Minus" => Some(MappedKey::Minus),282+ "Equal" => Some(MappedKey::Equal),283+ "BracketLeft" => Some(MappedKey::LeftBrace),284+ "BracketRight" => Some(MappedKey::RightBrace),285+ "Backslash" => Some(MappedKey::Backslash),286+ "IntlBackslash" => Some(MappedKey::NonUsBackslash),287+ "IntlRo" => Some(MappedKey::Ro),288+ "IntlYen" => Some(MappedKey::Yen),289+ "Semicolon" => Some(MappedKey::Semicolon),290+ "Quote" => Some(MappedKey::Apostrophe),291+ "Backquote" => Some(MappedKey::Grave),292+ "Comma" => Some(MappedKey::Comma),293+ "Period" => Some(MappedKey::Dot),294+ "Slash" => Some(MappedKey::Slash),295+ "Enter" => Some(MappedKey::Enter),296+ "Tab" => Some(MappedKey::Tab),297+ "Space" => Some(MappedKey::Space),298+ "Backspace" => Some(MappedKey::Backspace),299+ "Escape" => Some(MappedKey::Escape),300+ "ShiftLeft" => Some(MappedKey::LeftShift),301+ "ShiftRight" => Some(MappedKey::RightShift),302+ "ControlLeft" => Some(MappedKey::LeftCtrl),303+ "ControlRight" => Some(MappedKey::RightCtrl),304+ "AltLeft" => Some(MappedKey::LeftAlt),305+ "AltRight" => Some(MappedKey::RightAlt),306+ "MetaLeft" => Some(MappedKey::LeftMeta),307+ "MetaRight" => Some(MappedKey::RightMeta),308+ "Shift" => Some(MappedKey::LeftShift),309+ "Control" => Some(MappedKey::LeftCtrl),310+ "Alt" => Some(MappedKey::LeftAlt),311+ "Meta" => Some(MappedKey::LeftMeta),312+ "ArrowUp" => Some(MappedKey::Up),313+ "ArrowDown" => Some(MappedKey::Down),314+ "ArrowLeft" => Some(MappedKey::Left),315+ "ArrowRight" => Some(MappedKey::Right),316+ "Up" => Some(MappedKey::Up),317+ "Down" => Some(MappedKey::Down),318+ "Left" => Some(MappedKey::Left),319+ "Right" => Some(MappedKey::Right),320+ "Home" => Some(MappedKey::Home),321+ "End" => Some(MappedKey::End),322+ "PageUp" => Some(MappedKey::PageUp),323+ "PageDown" => Some(MappedKey::PageDown),324+ "Insert" => Some(MappedKey::Insert),325+ "Delete" => Some(MappedKey::Delete),326+ "PrintScreen" => Some(MappedKey::PrintScreen),327+ "F1" => Some(MappedKey::F1),328+ "F2" => Some(MappedKey::F2),329+ "F3" => Some(MappedKey::F3),330+ "F4" => Some(MappedKey::F4),331+ "F5" => Some(MappedKey::F5),332+ "F6" => Some(MappedKey::F6),333+ "F7" => Some(MappedKey::F7),334+ "F8" => Some(MappedKey::F8),335+ "F9" => Some(MappedKey::F9),336+ "F10" => Some(MappedKey::F10),337+ "F11" => Some(MappedKey::F11),338+ "F12" => Some(MappedKey::F12),339+ _ => map_printable_us_key(value),340+ }341+}342+343+344+fn map_printable_us_key(value: &str) -> Option<MappedKey> {345+ let mut chars = value.chars();346+ let character = chars.next()?;347+ if chars.next().is_some() || !character.is_ascii() {348+ return None;349+ }350+ Some(match character {351+ 'a' | 'A' => MappedKey::A,352+ 'b' | 'B' => MappedKey::B,353+ 'c' | 'C' => MappedKey::C,354+ 'd' | 'D' => MappedKey::D,355+ 'e' | 'E' => MappedKey::E,356+ 'f' | 'F' => MappedKey::F,357+ 'g' | 'G' => MappedKey::G,358+ 'h' | 'H' => MappedKey::H,359+ 'i' | 'I' => MappedKey::I,360+ 'j' | 'J' => MappedKey::J,361+ 'k' | 'K' => MappedKey::K,362+ 'l' | 'L' => MappedKey::L,363+ 'm' | 'M' => MappedKey::M,364+ 'n' | 'N' => MappedKey::N,365+ 'o' | 'O' => MappedKey::O,366+ 'p' | 'P' => MappedKey::P,367+ 'q' | 'Q' => MappedKey::Q,368+ 'r' | 'R' => MappedKey::R,369+ 's' | 'S' => MappedKey::S,370+ 't' | 'T' => MappedKey::T,371+ 'u' | 'U' => MappedKey::U,372+ 'v' | 'V' => MappedKey::V,373+ 'w' | 'W' => MappedKey::W,374+ 'x' | 'X' => MappedKey::X,375+ 'y' | 'Y' => MappedKey::Y,376+ 'z' | 'Z' => MappedKey::Z,377+ '0' | ')' => MappedKey::Num0,378+ '1' | '!' => MappedKey::Num1,379+ '2' | '@' => MappedKey::Num2,380+ '3' | '#' => MappedKey::Num3,381+ '4' | '$' => MappedKey::Num4,382+ '5' | '%' => MappedKey::Num5,383+ '6' | '^' => MappedKey::Num6,384+ '7' | '&' => MappedKey::Num7,385+ '8' | '*' => MappedKey::Num8,386+ '9' | '(' => MappedKey::Num9,387+ '-' | '_' => MappedKey::Minus,388+ '=' | '+' => MappedKey::Equal,389+ '[' | '{' => MappedKey::LeftBrace,390+ ']' | '}' => MappedKey::RightBrace,391+ '\\' | '|' => MappedKey::Backslash,392+ ';' | ':' => MappedKey::Semicolon,393+ '\'' | '"' => MappedKey::Apostrophe,394+ '`' | '~' => MappedKey::Grave,395+ ',' | '<' => MappedKey::Comma,396+ '.' | '>' => MappedKey::Dot,397+ '/' | '?' => MappedKey::Slash,398+ ' ' => MappedKey::Space,399+ _ => return None,400+ })401+}402+403+404+405+pub(crate) enum GamepadButton {406+ A,407+ B,408+ X,409+ Y,410+ LeftShoulder,411+ RightShoulder,412+ Back,413+ Start,414+ Guide,415+ LeftStick,416+ RightStick,417+}418+419+420+421+pub(crate) enum GamepadAxis {422+ LeftX,423+ LeftY,424+ RightX,425+ RightY,426+ LeftTrigger,427+ RightTrigger,428+ DpadX,429+ DpadY,430+}431+432+433+434+pub(crate) struct GamepadReport {435+ pub(crate) buttons: [(GamepadButton, bool); 11],436+ pub(crate) axes: [(GamepadAxis, i32); 8],437+}438+439+440+pub(crate) fn map_gamepad_state(state: &GamepadState) -> GamepadReport {441+ GamepadReport {442+ buttons: [443+ (GamepadButton::A, state.a),444+ (GamepadButton::B, state.b),445+ (GamepadButton::X, state.x),446+ (GamepadButton::Y, state.y),447+ (GamepadButton::LeftShoulder, state.left_shoulder),448+ (GamepadButton::RightShoulder, state.right_shoulder),449+ (GamepadButton::Back, state.back),450+ (GamepadButton::Start, state.start),451+ (GamepadButton::Guide, state.guide),452+ (GamepadButton::LeftStick, state.left_stick),453+ (GamepadButton::RightStick, state.right_stick),454+ ],455+ axes: [456+ (GamepadAxis::LeftX, i32::from(state.left_x)),457+ (GamepadAxis::LeftY, i32::from(state.left_y)),458+ (GamepadAxis::RightX, i32::from(state.right_x)),459+ (GamepadAxis::RightY, i32::from(state.right_y)),460+ (GamepadAxis::LeftTrigger, i32::from(state.left_trigger)),461+ (GamepadAxis::RightTrigger, i32::from(state.right_trigger)),462+ (463+ GamepadAxis::DpadX,464+ i32::from(state.dpad_right) - i32::from(state.dpad_left),465+ ),466+ (467+ GamepadAxis::DpadY,468+ i32::from(state.dpad_down) - i32::from(state.dpad_up),469+ ),470+ ],471+ }472+}473+474+475+pub(crate) fn linux_key(key: MappedKey) -> Key {476+ match key {477+ MappedKey::A => Key::A,478+ MappedKey::B => Key::B,479+ MappedKey::C => Key::C,480+ MappedKey::D => Key::D,481+ MappedKey::E => Key::E,482+ MappedKey::F => Key::F,483+ MappedKey::G => Key::G,484+ MappedKey::H => Key::H,485+ MappedKey::I => Key::I,486+ MappedKey::J => Key::J,487+ MappedKey::K => Key::K,488+ MappedKey::L => Key::L,489+ MappedKey::M => Key::M,490+ MappedKey::N => Key::N,491+ MappedKey::O => Key::O,492+ MappedKey::P => Key::P,493+ MappedKey::Q => Key::Q,494+ MappedKey::R => Key::R,495+ MappedKey::S => Key::S,496+ MappedKey::T => Key::T,497+ MappedKey::U => Key::U,498+ MappedKey::V => Key::V,499+ MappedKey::W => Key::W,500+ MappedKey::X => Key::X,501+ MappedKey::Y => Key::Y,502+ MappedKey::Z => Key::Z,503+ MappedKey::Num0 => Key::Num0,504+ MappedKey::Num1 => Key::Num1,505+ MappedKey::Num2 => Key::Num2,506+ MappedKey::Num3 => Key::Num3,507+ MappedKey::Num4 => Key::Num4,508+ MappedKey::Num5 => Key::Num5,509+ MappedKey::Num6 => Key::Num6,510+ MappedKey::Num7 => Key::Num7,511+ MappedKey::Num8 => Key::Num8,512+ MappedKey::Num9 => Key::Num9,513+ MappedKey::Minus => Key::Minus,514+ MappedKey::Equal => Key::Equal,515+ MappedKey::LeftBrace => Key::LeftBrace,516+ MappedKey::RightBrace => Key::RightBrace,517+ MappedKey::Backslash => Key::Backslash,518+ MappedKey::Semicolon => Key::Semicolon,519+ MappedKey::Apostrophe => Key::Apostrophe,520+ MappedKey::Grave => Key::Grave,521+ MappedKey::Comma => Key::Comma,522+ MappedKey::Dot => Key::Dot,523+ MappedKey::Slash => Key::Slash,524+ MappedKey::NonUsBackslash => Key::NonUsBackslashAndPipe,525+ MappedKey::Ro => Key::Ro,526+ MappedKey::Yen => Key::Yen,527+ MappedKey::Enter => Key::Enter,528+ MappedKey::Tab => Key::Tab,529+ MappedKey::Space => Key::Space,530+ MappedKey::Backspace => Key::Backspace,531+ MappedKey::Escape => Key::Esc,532+ MappedKey::LeftShift => Key::LeftShift,533+ MappedKey::RightShift => Key::RightShift,534+ MappedKey::LeftCtrl => Key::LeftCtrl,535+ MappedKey::RightCtrl => Key::RightCtrl,536+ MappedKey::LeftAlt => Key::LeftAlt,537+ MappedKey::RightAlt => Key::RightAlt,538+ MappedKey::LeftMeta => Key::LeftMeta,539+ MappedKey::RightMeta => Key::RightMeta,540+ MappedKey::Up => Key::Up,541+ MappedKey::Down => Key::Down,542+ MappedKey::Left => Key::Left,543+ MappedKey::Right => Key::Right,544+ MappedKey::Home => Key::Home,545+ MappedKey::End => Key::End,546+ MappedKey::PageUp => Key::PageUp,547+ MappedKey::PageDown => Key::PageDown,548+ MappedKey::Insert => Key::Insert,549+ MappedKey::Delete => Key::Delete,550+ MappedKey::PrintScreen => Key::Sysrq,551+ MappedKey::F1 => Key::F1,552+ MappedKey::F2 => Key::F2,553+ MappedKey::F3 => Key::F3,554+ MappedKey::F4 => Key::F4,555+ MappedKey::F5 => Key::F5,556+ MappedKey::F6 => Key::F6,557+ MappedKey::F7 => Key::F7,558+ MappedKey::F8 => Key::F8,559+ MappedKey::F9 => Key::F9,560+ MappedKey::F10 => Key::F10,561+ MappedKey::F11 => Key::F11,562+ MappedKey::F12 => Key::F12,563+ }564+}565+566+567+mod tests {568+ use super::*;569+570+ 571+ fn protocol_events_route_to_disjoint_virtual_devices() {572+ for event in [573+ InputEvent::MouseMove { x: 1, y: 2 },574+ InputEvent::MouseMoveRelative { dx: 1, dy: 2 },575+ InputEvent::MousePositionSync { x: 10, y: 20 },576+ InputEvent::MouseClick { b: 1 },577+ InputEvent::MouseDown { b: 2 },578+ InputEvent::MouseUp { b: 3 },579+ InputEvent::MouseScroll { dx: 1, dy: -1 },580+ ] {581+ assert_eq!(input_device_class(&event), Some(InputDeviceClass::Pointer));582+ }583+ for event in [584+ InputEvent::KeyDown { k: "a".into() },585+ InputEvent::KeyUp { k: "a".into() },586+ InputEvent::KeyClick { k: "a".into() },587+ ] {588+ assert_eq!(input_device_class(&event), Some(InputDeviceClass::Keyboard));589+ }590+ assert_eq!(591+ input_device_class(&InputEvent::Gamepad {592+ state: GamepadState::default(),593+ }),594+ Some(InputDeviceClass::Gamepad)595+ );596+ assert_eq!(597+ input_device_class(&InputEvent::Text {598+ s: "ignored".into()599+ }),600+ None601+ );602+ assert_eq!(input_device_class(&InputEvent::Probe), None);603+ }604+605+ 606+ fn maps_client_named_keys_without_guessing() {607+ assert_eq!(map_key("Enter"), Some(MappedKey::Enter));608+ assert_eq!(map_key("Control"), Some(MappedKey::LeftCtrl));609+ assert_eq!(map_key("PageDown"), Some(MappedKey::PageDown));610+ assert_eq!(map_key("F13"), None);611+ assert_eq!(map_key("CapsLock"), None);612+ assert_eq!(map_key("Numpad1"), None);613+ assert_eq!(map_key(""), None);614+ }615+616+ 617+ fn maps_bounded_browser_physical_keys_for_games_and_layouts() {618+ for (value, expected) in [619+ ("KeyQ", MappedKey::Q),620+ ("Digit2", MappedKey::Num2),621+ ("BracketLeft", MappedKey::LeftBrace),622+ ("IntlBackslash", MappedKey::NonUsBackslash),623+ ("IntlRo", MappedKey::Ro),624+ ("IntlYen", MappedKey::Yen),625+ ("ShiftLeft", MappedKey::LeftShift),626+ ("ShiftRight", MappedKey::RightShift),627+ ("ControlLeft", MappedKey::LeftCtrl),628+ ("ControlRight", MappedKey::RightCtrl),629+ ("AltLeft", MappedKey::LeftAlt),630+ ("AltRight", MappedKey::RightAlt),631+ ("MetaLeft", MappedKey::LeftMeta),632+ ("MetaRight", MappedKey::RightMeta),633+ ("ArrowUp", MappedKey::Up),634+ ("Insert", MappedKey::Insert),635+ ("PrintScreen", MappedKey::PrintScreen),636+ ("F1", MappedKey::F1),637+ ("F12", MappedKey::F12),638+ ] {639+ assert_eq!(map_key(value), Some(expected), "{value}");640+ assert!(ALL_MAPPED_KEYS.contains(&expected), "{value}");641+ }642+ let unique = ALL_MAPPED_KEYS643+ .iter()644+ .copied()645+ .collect::<std::collections::BTreeSet<_>>();646+ assert_eq!(unique.len(), ALL_MAPPED_KEYS.len());647+ }648+649+ 650+ 651+ fn extended_physical_keys_translate_to_exact_linux_evdev_codes() {652+ for (mapped, expected) in [653+ (MappedKey::F1, input_linux::Key::F1),654+ (MappedKey::F12, input_linux::Key::F12),655+ (MappedKey::Insert, input_linux::Key::Insert),656+ (MappedKey::PrintScreen, input_linux::Key::Sysrq),657+ (MappedKey::RightShift, input_linux::Key::RightShift),658+ (MappedKey::RightCtrl, input_linux::Key::RightCtrl),659+ (MappedKey::RightAlt, input_linux::Key::RightAlt),660+ (MappedKey::RightMeta, input_linux::Key::RightMeta),661+ (662+ MappedKey::NonUsBackslash,663+ input_linux::Key::NonUsBackslashAndPipe,664+ ),665+ (MappedKey::Ro, input_linux::Key::Ro),666+ (MappedKey::Yen, input_linux::Key::Yen),667+ ] {668+ assert_eq!(linux_key(mapped), expected);669+ }670+ }671+672+ 673+ fn maps_us_printable_keys_to_physical_keys() {674+ assert_eq!(map_key("a"), Some(MappedKey::A));675+ assert_eq!(map_key("A"), Some(MappedKey::A));676+ assert_eq!(map_key("!"), Some(MappedKey::Num1));677+ assert_eq!(map_key("?"), Some(MappedKey::Slash));678+ assert_eq!(map_key("Ă©"), None);679+ assert_eq!(map_key("ab"), None);680+ }681+682+ 683+ fn pointer_position_sync_uses_two_ordered_relative_reports() {684+ assert_eq!(685+ pointer_position_sync_report(1_280, 800),686+ [687+ PointerReportEvent::RelativeX(-32_767),688+ PointerReportEvent::RelativeY(-32_767),689+ PointerReportEvent::Synchronize,690+ PointerReportEvent::RelativeX(1_280),691+ PointerReportEvent::RelativeY(800),692+ PointerReportEvent::Synchronize,693+ ]694+ );695+ }696+697+ 698+ fn maps_every_gamepad_control_into_one_bounded_report() {699+ let state = GamepadState {700+ a: true,701+ b: true,702+ x: true,703+ y: true,704+ left_shoulder: true,705+ right_shoulder: true,706+ back: true,707+ start: true,708+ guide: true,709+ left_stick: true,710+ right_stick: true,711+ dpad_up: true,712+ dpad_right: true,713+ left_x: -32_767,714+ left_y: 32_767,715+ right_x: -123,716+ right_y: 456,717+ left_trigger: 12_345,718+ right_trigger: 32_767,719+ ..GamepadState::default()720+ };721+ let report = map_gamepad_state(&state);722+ assert_eq!(report.buttons.len(), 11);723+ assert!(report.buttons.into_iter().all(|(_, pressed)| pressed));724+ assert_eq!(725+ report.axes,726+ [727+ (GamepadAxis::LeftX, -32_767),728+ (GamepadAxis::LeftY, 32_767),729+ (GamepadAxis::RightX, -123),730+ (GamepadAxis::RightY, 456),731+ (GamepadAxis::LeftTrigger, 12_345),732+ (GamepadAxis::RightTrigger, 32_767),733+ (GamepadAxis::DpadX, 1),734+ (GamepadAxis::DpadY, -1),735+ ]736+ );737+ }738+739+ 740+ fn neutral_gamepad_report_releases_buttons_and_axes() {741+ let report = map_gamepad_state(&GamepadState::default());742+ assert!(report.buttons.into_iter().all(|(_, pressed)| !pressed));743+ assert!(report.axes.into_iter().all(|(_, value)| value == 0));744+ }745+}@@ -0,0 +1,691 @@
1+use std::collections::BTreeSet;2+use std::fs::{File, OpenOptions};3+use std::os::fd::AsRawFd;4+use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};5+6+use anyhow::{Context, Result, ensure};7+use input_linux::{8+ AbsoluteAxis, AbsoluteEvent, AbsoluteInfo, AbsoluteInfoSetup, EventKind, EventTime, InputId,9+ InputProperty, Key, KeyEvent, KeyState, RelativeAxis, RelativeEvent, SynchronizeEvent,10+ UInputHandle,11+};12+use sigil_protocol::{13+ GAMEPAD_AXIS_MAX, GAMEPAD_AXIS_MIN, GAMEPAD_TRIGGER_MAX, GamepadState,14+ InputEvent as ProtocolInputEvent,15+};16+17+use crate::config::UinputConfig;18+use crate::input::acl::{POSIX_ACL_REQUIRED_BYTES, validate_single_user_access_acl};19+use crate::input::keymap::{20+ ALL_MAPPED_KEYS, GamepadAxis, GamepadButton, InputDeviceClass, PointerReportEvent,21+ input_device_class, linux_key, map_gamepad_state, map_key, pointer_position_sync_report,22+};23+use crate::input::{24+ GAMEPAD_DEVICE_NAME, GAMEPAD_PRODUCT_ID, KEYBOARD_DEVICE_NAME, KEYBOARD_PRODUCT_ID,25+ POINTER_DEVICE_NAME, POINTER_PRODUCT_ID, UINPUT_BUS_TYPE, UINPUT_DEVICE_VERSION,26+ UINPUT_VENDOR_ID,27+};28+29+const UINPUT_MAJOR: u32 = 10;30+const UINPUT_MINOR: u32 = 223;31+const MAX_POINTER_EVENTS: usize = 6;32+const MAX_KEYBOARD_EVENTS: usize = ALL_MAPPED_KEYS.len() + 1;33+const GAMEPAD_REPORT_EVENTS: usize = 20;34+35+pub struct UinputDevice {36+ pointer: PointerDevice,37+ keyboard: KeyboardDevice,38+ gamepad: GamepadDevice,39+}40+41+impl UinputDevice {42+ pub fn open(config: &UinputConfig) -> Result<Self> {43+ // Each virtual device owns a distinct validated descriptor. This44+ // keeps libinput/udev classification from merging keyboard keys45+ // into the conventional relative pointer topology.46+ let pointer = PointerDevice::open(config)?;47+ let keyboard = KeyboardDevice::open(config)?;48+ let gamepad = GamepadDevice::open(config)?;49+50+ Ok(Self {51+ pointer,52+ keyboard,53+ gamepad,54+ })55+ }56+57+ pub fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {58+ match input_device_class(event) {59+ Some(InputDeviceClass::Pointer) => self.pointer.apply(event),60+ Some(InputDeviceClass::Keyboard) => self.keyboard.apply(event),61+ Some(InputDeviceClass::Gamepad) => match event {62+ ProtocolInputEvent::Gamepad { state } => self.gamepad.apply(state),63+ _ => unreachable!("input device classification is exhaustive"),64+ },65+ None => Ok(()),66+ }67+ }68+69+ pub fn release_all(&mut self) -> Result<()> {70+ // Evaluate every release before combining results so one broken71+ // descriptor cannot strand held state on either sibling device.72+ let pointer_result = self.pointer.release_all();73+ let keyboard_result = self.keyboard.release_all();74+ let gamepad_result = self.gamepad.neutralize();75+ pointer_result.and(keyboard_result).and(gamepad_result)76+ }77+}78+79+struct PointerDevice {80+ handle: UInputHandle<File>,81+ pressed: BTreeSet<Key>,82+}83+84+impl PointerDevice {85+ fn open(config: &UinputConfig) -> Result<Self> {86+ let handle = open_validated_handle(config, "pointer")?;87+ handle88+ .set_propbit(InputProperty::Pointer)89+ .context("enabling pointer property")?;90+ handle91+ .set_evbit(EventKind::Key)92+ .context("enabling pointer button events")?;93+ for key in [Key::ButtonLeft, Key::ButtonRight, Key::ButtonMiddle] {94+ handle95+ .set_keybit(key)96+ .context("enabling a bounded pointer button capability")?;97+ }98+ handle99+ .set_evbit(EventKind::Relative)100+ .context("enabling relative pointer events")?;101+ handle102+ .set_relbit(RelativeAxis::X)103+ .context("enabling relative X events")?;104+ handle105+ .set_relbit(RelativeAxis::Y)106+ .context("enabling relative Y events")?;107+ handle108+ .set_relbit(RelativeAxis::Wheel)109+ .context("enabling vertical wheel events")?;110+ handle111+ .set_relbit(RelativeAxis::HorizontalWheel)112+ .context("enabling horizontal wheel events")?;113+ handle114+ .create(115+ &InputId {116+ bustype: UINPUT_BUS_TYPE,117+ vendor: UINPUT_VENDOR_ID,118+ product: POINTER_PRODUCT_ID,119+ version: UINPUT_DEVICE_VERSION,120+ },121+ POINTER_DEVICE_NAME,122+ 0,123+ &[],124+ )125+ .context("registering Sigil virtual pointer")?;126+127+ Ok(Self {128+ handle,129+ pressed: BTreeSet::new(),130+ })131+ }132+133+ fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {134+ match event {135+ ProtocolInputEvent::MouseMoveRelative { dx, dy } => {136+ let mut events = [synchronize_event(); 3];137+ let mut len = 0;138+ if *dx != 0 {139+ events[len] = relative_event(RelativeAxis::X, *dx);140+ len += 1;141+ }142+ if *dy != 0 {143+ events[len] = relative_event(RelativeAxis::Y, *dy);144+ len += 1;145+ }146+ if len == 0 {147+ return Ok(());148+ }149+ events[len] = synchronize_event();150+ self.emit(&events[..=len])151+ }152+ ProtocolInputEvent::MousePositionSync { x, y } => {153+ let events = pointer_position_sync_report(*x, *y).map(|event| match event {154+ PointerReportEvent::RelativeX(value) => relative_event(RelativeAxis::X, value),155+ PointerReportEvent::RelativeY(value) => relative_event(RelativeAxis::Y, value),156+ PointerReportEvent::Synchronize => synchronize_event(),157+ });158+ self.emit(&events)159+ }160+ ProtocolInputEvent::MouseMove { .. } => anyhow::bail!(161+ "absolute pointer movement is unavailable on the relative-only uinput device"162+ ),163+ ProtocolInputEvent::MouseClick { b } => {164+ self.click(mouse_button(*b).context("unsupported mouse button")?)165+ }166+ ProtocolInputEvent::MouseDown { b } => {167+ self.press(mouse_button(*b).context("unsupported mouse button")?)168+ }169+ ProtocolInputEvent::MouseUp { b } => {170+ self.release(mouse_button(*b).context("unsupported mouse button")?)171+ }172+ ProtocolInputEvent::MouseScroll { dx, dy } => {173+ let vertical = dy174+ .checked_neg()175+ .context("vertical wheel delta is out of range")?;176+ let mut events = [synchronize_event(); 3];177+ let mut len = 0;178+ if *dx != 0 {179+ events[len] = relative_event(RelativeAxis::HorizontalWheel, *dx);180+ len += 1;181+ }182+ if vertical != 0 {183+ events[len] = relative_event(RelativeAxis::Wheel, vertical);184+ len += 1;185+ }186+ if len == 0 {187+ return Ok(());188+ }189+ events[len] = synchronize_event();190+ self.emit(&events[..=len])191+ }192+ _ => unreachable!("only pointer events are routed to the pointer device"),193+ }194+ }195+196+ fn press(&mut self, key: Key) -> Result<()> {197+ self.emit(&[key_event(key, KeyState::PRESSED), synchronize_event()])?;198+ self.pressed.insert(key);199+ Ok(())200+ }201+202+ fn release(&mut self, key: Key) -> Result<()> {203+ self.emit(&[key_event(key, KeyState::RELEASED), synchronize_event()])?;204+ self.pressed.remove(&key);205+ Ok(())206+ }207+208+ fn click(&mut self, key: Key) -> Result<()> {209+ ensure!(210+ !self.pressed.contains(&key),211+ "click transition is invalid while the key or button is held"212+ );213+ self.emit(&[214+ key_event(key, KeyState::PRESSED),215+ synchronize_event(),216+ key_event(key, KeyState::RELEASED),217+ synchronize_event(),218+ ])219+ }220+221+ fn release_all(&mut self) -> Result<()> {222+ if self.pressed.is_empty() {223+ return Ok(());224+ }225+ let mut events = Vec::with_capacity(self.pressed.len() + 1);226+ events.extend(227+ self.pressed228+ .iter()229+ .copied()230+ .map(|key| key_event(key, KeyState::RELEASED)),231+ );232+ events.push(synchronize_event());233+ ensure!(234+ events.len() <= MAX_POINTER_EVENTS,235+ "pointer release set exceeded its static bound"236+ );237+ self.emit(&events)?;238+ self.pressed.clear();239+ Ok(())240+ }241+242+ fn emit(&self, events: &[input_linux::sys::input_event]) -> Result<()> {243+ ensure!(244+ !events.is_empty() && events.len() <= MAX_POINTER_EVENTS,245+ "pointer event batch is outside its static bound"246+ );247+ let written = self248+ .handle249+ .write(events)250+ .context("writing nonblocking pointer event batch")?;251+ ensure!(252+ written == events.len(),253+ "uinput accepted only {written} of {} pointer events",254+ events.len()255+ );256+ Ok(())257+ }258+}259+260+impl Drop for PointerDevice {261+ fn drop(&mut self) {262+ let _ = self.release_all();263+ let _ = self.handle.dev_destroy();264+ }265+}266+267+struct KeyboardDevice {268+ handle: UInputHandle<File>,269+ pressed: BTreeSet<Key>,270+}271+272+impl KeyboardDevice {273+ fn open(config: &UinputConfig) -> Result<Self> {274+ let handle = open_validated_handle(config, "keyboard")?;275+ handle276+ .set_evbit(EventKind::Key)277+ .context("enabling keyboard key events")?;278+ for key in ALL_MAPPED_KEYS.iter().copied().map(linux_key) {279+ handle280+ .set_keybit(key)281+ .context("enabling a bounded keyboard key capability")?;282+ }283+ handle284+ .create(285+ &InputId {286+ bustype: UINPUT_BUS_TYPE,287+ vendor: UINPUT_VENDOR_ID,288+ product: KEYBOARD_PRODUCT_ID,289+ version: UINPUT_DEVICE_VERSION,290+ },291+ KEYBOARD_DEVICE_NAME,292+ 0,293+ &[],294+ )295+ .context("registering Sigil virtual keyboard")?;296+297+ Ok(Self {298+ handle,299+ pressed: BTreeSet::new(),300+ })301+ }302+303+ fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {304+ match event {305+ ProtocolInputEvent::KeyDown { k } => {306+ self.press(linux_key(map_key(k).context("unsupported keyboard key")?))307+ }308+ ProtocolInputEvent::KeyUp { k } => {309+ self.release(linux_key(map_key(k).context("unsupported keyboard key")?))310+ }311+ ProtocolInputEvent::KeyClick { k } => {312+ self.click(linux_key(map_key(k).context("unsupported keyboard key")?))313+ }314+ _ => unreachable!("only keyboard events are routed to the keyboard device"),315+ }316+ }317+318+ fn press(&mut self, key: Key) -> Result<()> {319+ self.emit(&[key_event(key, KeyState::PRESSED), synchronize_event()])?;320+ self.pressed.insert(key);321+ Ok(())322+ }323+324+ fn release(&mut self, key: Key) -> Result<()> {325+ self.emit(&[key_event(key, KeyState::RELEASED), synchronize_event()])?;326+ self.pressed.remove(&key);327+ Ok(())328+ }329+330+ fn click(&mut self, key: Key) -> Result<()> {331+ ensure!(332+ !self.pressed.contains(&key),333+ "click transition is invalid while the key is held"334+ );335+ self.emit(&[336+ key_event(key, KeyState::PRESSED),337+ synchronize_event(),338+ key_event(key, KeyState::RELEASED),339+ synchronize_event(),340+ ])341+ }342+343+ fn release_all(&mut self) -> Result<()> {344+ if self.pressed.is_empty() {345+ return Ok(());346+ }347+ let mut events = Vec::with_capacity(self.pressed.len() + 1);348+ events.extend(349+ self.pressed350+ .iter()351+ .copied()352+ .map(|key| key_event(key, KeyState::RELEASED)),353+ );354+ events.push(synchronize_event());355+ ensure!(356+ events.len() <= MAX_KEYBOARD_EVENTS,357+ "keyboard release set exceeded its static bound"358+ );359+ self.emit(&events)?;360+ self.pressed.clear();361+ Ok(())362+ }363+364+ fn emit(&self, events: &[input_linux::sys::input_event]) -> Result<()> {365+ ensure!(366+ !events.is_empty() && events.len() <= MAX_KEYBOARD_EVENTS,367+ "keyboard event batch is outside its static bound"368+ );369+ let written = self370+ .handle371+ .write(events)372+ .context("writing nonblocking keyboard event batch")?;373+ ensure!(374+ written == events.len(),375+ "uinput accepted only {written} of {} keyboard events",376+ events.len()377+ );378+ Ok(())379+ }380+}381+382+impl Drop for KeyboardDevice {383+ fn drop(&mut self) {384+ let _ = self.release_all();385+ let _ = self.handle.dev_destroy();386+ }387+}388+389+struct GamepadDevice {390+ handle: UInputHandle<File>,391+}392+393+impl GamepadDevice {394+ fn open(config: &UinputConfig) -> Result<Self> {395+ let handle = open_validated_handle(config, "gamepad")?;396+397+ handle398+ .set_evbit(EventKind::Key)399+ .context("enabling gamepad key events")?;400+ for (button, _) in map_gamepad_state(&GamepadState::default()).buttons {401+ handle402+ .set_keybit(linux_gamepad_button(button))403+ .context("enabling a bounded gamepad button capability")?;404+ }405+ handle406+ .set_evbit(EventKind::Absolute)407+ .context("enabling gamepad absolute-axis events")?;408+ for (axis, _) in map_gamepad_state(&GamepadState::default()).axes {409+ handle410+ .set_absbit(linux_gamepad_axis(axis))411+ .context("enabling a bounded gamepad axis capability")?;412+ }413+414+ let axes = [415+ absolute_range(416+ AbsoluteAxis::X,417+ i32::from(GAMEPAD_AXIS_MIN),418+ i32::from(GAMEPAD_AXIS_MAX),419+ ),420+ absolute_range(421+ AbsoluteAxis::Y,422+ i32::from(GAMEPAD_AXIS_MIN),423+ i32::from(GAMEPAD_AXIS_MAX),424+ ),425+ absolute_range(426+ AbsoluteAxis::RX,427+ i32::from(GAMEPAD_AXIS_MIN),428+ i32::from(GAMEPAD_AXIS_MAX),429+ ),430+ absolute_range(431+ AbsoluteAxis::RY,432+ i32::from(GAMEPAD_AXIS_MIN),433+ i32::from(GAMEPAD_AXIS_MAX),434+ ),435+ absolute_range(AbsoluteAxis::Z, 0, i32::from(GAMEPAD_TRIGGER_MAX)),436+ absolute_range(AbsoluteAxis::RZ, 0, i32::from(GAMEPAD_TRIGGER_MAX)),437+ absolute_range(AbsoluteAxis::Hat0X, -1, 1),438+ absolute_range(AbsoluteAxis::Hat0Y, -1, 1),439+ ];440+ handle441+ .create(442+ &InputId {443+ bustype: UINPUT_BUS_TYPE,444+ vendor: UINPUT_VENDOR_ID,445+ product: GAMEPAD_PRODUCT_ID,446+ version: UINPUT_DEVICE_VERSION,447+ },448+ GAMEPAD_DEVICE_NAME,449+ 0,450+ &axes,451+ )452+ .context("registering Sigil virtual gamepad")?;453+ let device = Self { handle };454+ device.neutralize()?;455+ Ok(device)456+ }457+458+ fn apply(&self, state: &GamepadState) -> Result<()> {459+ state.validate().context("validating gamepad snapshot")?;460+ let report = map_gamepad_state(state);461+ let mut events = [synchronize_event(); GAMEPAD_REPORT_EVENTS];462+ let mut index = 0;463+ for (button, pressed) in report.buttons {464+ events[index] = key_event(linux_gamepad_button(button), KeyState::pressed(pressed));465+ index += 1;466+ }467+ for (axis, value) in report.axes {468+ events[index] = absolute_event(linux_gamepad_axis(axis), value);469+ index += 1;470+ }471+ events[index] = synchronize_event();472+ index += 1;473+ ensure!(474+ index == GAMEPAD_REPORT_EVENTS,475+ "gamepad report violated its static event bound"476+ );477+ let written = self478+ .handle479+ .write(&events)480+ .context("writing nonblocking gamepad snapshot")?;481+ ensure!(482+ written == events.len(),483+ "uinput accepted only {written} of {} gamepad events",484+ events.len()485+ );486+ Ok(())487+ }488+489+ fn neutralize(&self) -> Result<()> {490+ self.apply(&GamepadState::default())491+ .context("neutralizing virtual gamepad")492+ }493+}494+495+impl Drop for GamepadDevice {496+ fn drop(&mut self) {497+ let _ = self.neutralize();498+ let _ = self.handle.dev_destroy();499+ }500+}501+502+fn open_validated_handle(config: &UinputConfig, device_class: &str) -> Result<UInputHandle<File>> {503+ let handle = UInputHandle::new(open_validated_device(config)?);504+ let version = handle505+ .version()506+ .with_context(|| format!("querying uinput interface version for {device_class}"))?;507+ ensure!(508+ version >= 5,509+ "uinput interface version {version} is too old for {device_class}"510+ );511+ Ok(handle)512+}513+514+fn open_validated_device(config: &UinputConfig) -> Result<File> {515+ let mut options = OpenOptions::new();516+ options517+ .read(true)518+ .write(true)519+ .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK);520+ let file = options.open(&config.device_path).with_context(|| {521+ format!(522+ "opening configured uinput device {} without following symlinks",523+ config.device_path.display()524+ )525+ })?;526+ validate_open_device(&file, config)?;527+ Ok(file)528+}529+530+fn validate_open_device(file: &File, config: &UinputConfig) -> Result<()> {531+ let metadata = file.metadata().with_context(|| {532+ format!(533+ "inspecting opened uinput device {}",534+ config.device_path.display()535+ )536+ })?;537+ ensure!(538+ metadata.file_type().is_char_device(),539+ "configured uinput path is not a character device"540+ );541+ ensure!(542+ metadata.uid() == config.expected_owner_uid,543+ "configured uinput owner UID changed"544+ );545+ ensure!(546+ metadata.gid() == config.expected_group_gid,547+ "configured uinput group GID changed"548+ );549+ ensure!(550+ metadata.mode() & 0o7777 == config.expected_mode,551+ "configured uinput permission mode changed"552+ );553+ let device = metadata.rdev();554+ ensure!(555+ libc::major(device) == UINPUT_MAJOR && libc::minor(device) == UINPUT_MINOR,556+ "configured character device is not the Linux uinput misc device"557+ );558+ validate_access_acl(file, config)?;559+ Ok(())560+}561+562+fn validate_access_acl(file: &File, config: &UinputConfig) -> Result<()> {563+ if let Some(expected_uid) = config.expected_acl_user_uid {564+ let effective_uid = unsafe { libc::geteuid() };565+ ensure!(566+ expected_uid == effective_uid,567+ "configured uinput ACL user UID {expected_uid} does not match Sigil effective UID {effective_uid}"568+ );569+ }570+571+ let attribute = c"system.posix_acl_access";572+ let mut bytes = [0_u8; POSIX_ACL_REQUIRED_BYTES];573+ let result = unsafe {574+ libc::fgetxattr(575+ file.as_raw_fd(),576+ attribute.as_ptr(),577+ bytes.as_mut_ptr().cast(),578+ bytes.len(),579+ )580+ };581+ if result >= 0 {582+ let Some(expected_uid) = config.expected_acl_user_uid else {583+ anyhow::bail!(584+ "configured uinput device must use the group-only scheme with no extended access ACL; set uinput.expected_acl_user_uid only for the explicit one-user ACL scheme"585+ );586+ };587+ let length =588+ usize::try_from(result).context("converting configured uinput access ACL length")?;589+ return validate_single_user_access_acl(590+ bytes591+ .get(..length)592+ .context("configured uinput access ACL exceeded its fixed bound")?,593+ expected_uid,594+ config.expected_mode,595+ )596+ .context("validating configured uinput one-user access ACL");597+ }598+ let error = std::io::Error::last_os_error();599+ if error600+ .raw_os_error()601+ .is_some_and(|code| code == libc::ENODATA || code == libc::ENOTSUP)602+ {603+ ensure!(604+ config.expected_acl_user_uid.is_none(),605+ "configured uinput device is missing the explicitly required one-user access ACL"606+ );607+ return Ok(());608+ }609+ if error.raw_os_error() == Some(libc::ERANGE) {610+ if config.expected_acl_user_uid.is_some() {611+ anyhow::bail!("configured uinput access ACL exceeds the exact one-user ACL scheme");612+ }613+ anyhow::bail!(614+ "configured uinput device must use the group-only scheme with no extended access ACL"615+ );616+ }617+ Err(error).context("checking configured uinput device access ACL")618+}619+620+fn absolute_range(axis: AbsoluteAxis, minimum: i32, maximum: i32) -> AbsoluteInfoSetup {621+ AbsoluteInfoSetup {622+ axis,623+ info: AbsoluteInfo {624+ minimum,625+ maximum,626+ ..AbsoluteInfo::default()627+ },628+ }629+}630+631+fn mouse_button(button: u8) -> Option<Key> {632+ match button {633+ 1 => Some(Key::ButtonLeft),634+ 2 => Some(Key::ButtonRight),635+ 3 => Some(Key::ButtonMiddle),636+ _ => None,637+ }638+}639+640+fn linux_gamepad_button(button: GamepadButton) -> Key {641+ match button {642+ GamepadButton::A => Key::ButtonSouth,643+ GamepadButton::B => Key::ButtonEast,644+ GamepadButton::X => Key::ButtonNorth,645+ GamepadButton::Y => Key::ButtonWest,646+ GamepadButton::LeftShoulder => Key::ButtonTL,647+ GamepadButton::RightShoulder => Key::ButtonTR,648+ GamepadButton::Back => Key::ButtonSelect,649+ GamepadButton::Start => Key::ButtonStart,650+ GamepadButton::Guide => Key::ButtonMode,651+ GamepadButton::LeftStick => Key::ButtonThumbl,652+ GamepadButton::RightStick => Key::ButtonThumbr,653+ }654+}655+656+fn linux_gamepad_axis(axis: GamepadAxis) -> AbsoluteAxis {657+ match axis {658+ GamepadAxis::LeftX => AbsoluteAxis::X,659+ GamepadAxis::LeftY => AbsoluteAxis::Y,660+ GamepadAxis::RightX => AbsoluteAxis::RX,661+ GamepadAxis::RightY => AbsoluteAxis::RY,662+ GamepadAxis::LeftTrigger => AbsoluteAxis::Z,663+ GamepadAxis::RightTrigger => AbsoluteAxis::RZ,664+ GamepadAxis::DpadX => AbsoluteAxis::Hat0X,665+ GamepadAxis::DpadY => AbsoluteAxis::Hat0Y,666+ }667+}668+669+fn key_event(key: Key, state: KeyState) -> input_linux::sys::input_event {670+ KeyEvent::new(EventTime::default(), key, state)671+ .into_event()672+ .into_raw()673+}674+675+fn absolute_event(axis: AbsoluteAxis, value: i32) -> input_linux::sys::input_event {676+ AbsoluteEvent::new(EventTime::default(), axis, value)677+ .into_event()678+ .into_raw()679+}680+681+fn relative_event(axis: RelativeAxis, value: i32) -> input_linux::sys::input_event {682+ RelativeEvent::new(EventTime::default(), axis, value)683+ .into_event()684+ .into_raw()685+}686+687+fn synchronize_event() -> input_linux::sys::input_event {688+ SynchronizeEvent::report(EventTime::default())689+ .into_event()690+ .into_raw()691+}@@ -0,0 +1,351 @@
1+use std::fmt;2+3+use std::sync::Arc;4+5+use std::sync::Mutex;6+7+8+use anyhow::Context;9+use anyhow::{Result, bail};10+use sigil_protocol::{Capability, InputEvent};11+12+use crate::config::{HostConfig, InputMode};13+14+mod acl;15+mod keymap;16+17+mod linux;18+19+const ACK_ONLY_CAPABILITIES: &[Capability] = &[Capability::InputAck];20+const UINPUT_CAPABILITIES: &[Capability] = &[21+ Capability::RelativePointer,22+ Capability::Keyboard,23+ Capability::Gamepad,24+ Capability::InputAck,25+];26+27+28+// This Linux input identity tuple is an external ABI consumed by Gamescope,29+// udev/libinput rules, and hardware evidence. Preserve it exactly; see30+// docs/compatibility-identifiers.md.31+const UINPUT_BUS_TYPE: u16 = 0x06; // Linux BUS_VIRTUAL32+33+const UINPUT_VENDOR_ID: u16 = 0x5347;34+35+const UINPUT_DEVICE_VERSION: u16 = 1;36+37+const POINTER_PRODUCT_ID: u16 = 1;38+39+const GAMEPAD_PRODUCT_ID: u16 = 2;40+41+const KEYBOARD_PRODUCT_ID: u16 = 3;42+43+const POINTER_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Pointer";44+45+const KEYBOARD_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Keyboard";46+47+const GAMEPAD_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Gamepad";48+49+50+pub enum InputDisposition {51+ Probed,52+ Disabled,53+ Observed,54+ 55+ Injected,56+ TextIgnored,57+}58+59+60+pub struct InputBackend {61+ mode: InputMode,62+ 63+ device: Option<Arc<std::sync::Mutex<linux::UinputDevice>>>,64+}65+66+impl fmt::Debug for InputBackend {67+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {68+ formatter69+ .debug_struct("InputBackend")70+ .field("mode", &self.mode)71+ .finish_non_exhaustive()72+ }73+}74+75+impl InputBackend {76+ /// Construct the backend before the network endpoint is exposed. For77+ /// uinput this opens, validates, and registers the virtual device.78+ pub fn initialize(config: &HostConfig) -> Result<Self> {79+ match config.input_mode {80+ InputMode::Disabled | InputMode::Log => Ok(Self {81+ mode: config.input_mode.clone(),82+ 83+ device: None,84+ }),85+ InputMode::Uinput => {86+ 87+ {88+ let settings = config89+ .uinput90+ .as_ref()91+ .context("missing validated uinput configuration")?;92+ let device = linux::UinputDevice::open(settings)?;93+ Ok(Self {94+ mode: InputMode::Uinput,95+ device: Some(Arc::new(std::sync::Mutex::new(device))),96+ })97+ }98+ 99+ {100+ let _ = config;101+ bail!("input_mode uinput is supported only on Linux")102+ }103+ }104+ }105+ }106+107+ pub fn capabilities(&self) -> &'static [Capability] {108+ capabilities_for_mode(&self.mode)109+ }110+111+ pub fn apply(&self, event: &InputEvent, negotiated: &[Capability]) -> Result<InputDisposition> {112+ ensure_event_was_negotiated(&self.mode, event, negotiated)?;113+ if matches!(event, InputEvent::Probe) {114+ return Ok(InputDisposition::Probed);115+ }116+ match self.mode {117+ InputMode::Disabled => Ok(InputDisposition::Disabled),118+ InputMode::Log => Ok(InputDisposition::Observed),119+ InputMode::Uinput => {120+ if matches!(event, InputEvent::Text { .. }) {121+ return Ok(InputDisposition::TextIgnored);122+ }123+ 124+ {125+ let device = self126+ .device127+ .as_ref()128+ .context("uinput backend was not initialized")?;129+ device130+ .lock()131+ .map_err(|_| anyhow::anyhow!("uinput backend lock poisoned"))?132+ .apply(event)?;133+ Ok(InputDisposition::Injected)134+ }135+ 136+ {137+ let _ = event;138+ bail!("uinput backend is unavailable on this platform")139+ }140+ }141+ }142+ }143+144+ /// Release any transitions held by the ending input session so a dropped145+ /// connection cannot leave a key or mouse button pressed.146+ pub fn reset_session(&self) -> Result<()> {147+ 148+ if let Some(device) = &self.device {149+ reset_through_poisoned_lock(device, linux::UinputDevice::release_all)?;150+ }151+ Ok(())152+ }153+}154+155+156+fn reset_through_poisoned_lock<T>(157+ device: &Mutex<T>,158+ reset: impl FnOnce(&mut T) -> Result<()>,159+) -> Result<()> {160+ let mut device = match device.lock() {161+ Ok(device) => device,162+ Err(poisoned) => {163+ // A panic while applying input poisons this mutex. Teardown must164+ // still recover the device solely to release held transitions;165+ // ordinary input continues to reject the poisoned lock.166+ tracing::warn!("recovering poisoned uinput lock for session reset");167+ poisoned.into_inner()168+ }169+ };170+ reset(&mut device)171+}172+173+fn ensure_event_was_negotiated(174+ mode: &InputMode,175+ event: &InputEvent,176+ negotiated: &[Capability],177+) -> Result<()> {178+ if !matches!(mode, InputMode::Uinput) {179+ return Ok(());180+ }181+ let required = match event {182+ InputEvent::Probe => Some(Capability::InputAck),183+ InputEvent::MouseMove { .. } => Some(Capability::AbsolutePointer),184+ InputEvent::MouseMoveRelative { .. }185+ | InputEvent::MousePositionSync { .. }186+ | InputEvent::MouseClick { .. }187+ | InputEvent::MouseDown { .. }188+ | InputEvent::MouseUp { .. }189+ | InputEvent::MouseScroll { .. } => Some(Capability::RelativePointer),190+ InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } | InputEvent::KeyClick { .. } => {191+ Some(Capability::Keyboard)192+ }193+ InputEvent::Gamepad { .. } => Some(Capability::Gamepad),194+ // Text remains an explicit content-free no-op for compatibility with195+ // clients that emit it alongside negotiated physical key transitions.196+ InputEvent::Text { .. } => None,197+ };198+ if required.is_some_and(|capability| !negotiated.contains(&capability)) {199+ bail!("input event class was not negotiated for this session");200+ }201+ Ok(())202+}203+204+fn capabilities_for_mode(mode: &InputMode) -> &'static [Capability] {205+ match mode {206+ InputMode::Disabled | InputMode::Log => ACK_ONLY_CAPABILITIES,207+ InputMode::Uinput => UINPUT_CAPABILITIES,208+ }209+}210+211+212+mod tests {213+ use std::panic::{AssertUnwindSafe, catch_unwind};214+ use std::sync::Arc;215+216+ use sigil_protocol::GamepadState;217+218+ use super::*;219+220+ 221+ fn virtual_input_topology_has_stable_distinct_identities() {222+ assert_eq!(UINPUT_BUS_TYPE, 0x06);223+ 224+ assert_eq!(UINPUT_BUS_TYPE, input_linux::sys::BUS_VIRTUAL);225+ assert_eq!(UINPUT_VENDOR_ID, 0x5347);226+ assert_eq!(UINPUT_DEVICE_VERSION, 1);227+ assert_eq!(POINTER_DEVICE_NAME, b"Sigil Spark Virtual Pointer");228+ assert_eq!(KEYBOARD_DEVICE_NAME, b"Sigil Spark Virtual Keyboard");229+ assert_eq!(GAMEPAD_DEVICE_NAME, b"Sigil Spark Virtual Gamepad");230+ assert_eq!(POINTER_PRODUCT_ID, 1);231+ assert_eq!(GAMEPAD_PRODUCT_ID, 2);232+ assert_eq!(KEYBOARD_PRODUCT_ID, 3);233+ assert_ne!(POINTER_PRODUCT_ID, KEYBOARD_PRODUCT_ID);234+ assert_ne!(POINTER_PRODUCT_ID, GAMEPAD_PRODUCT_ID);235+ assert_ne!(KEYBOARD_PRODUCT_ID, GAMEPAD_PRODUCT_ID);236+ }237+238+ 239+ fn session_reset_recovers_a_lock_poisoned_while_applying_input() {240+ 241+ struct DeviceState {242+ released: bool,243+ }244+245+ let device = Arc::new(Mutex::new(DeviceState::default()));246+ let panic_device = Arc::clone(&device);247+ let panic_result = catch_unwind(AssertUnwindSafe(move || {248+ let _device = panic_device.lock().unwrap();249+ panic!("simulate panic while applying uinput");250+ }));251+ assert!(panic_result.is_err());252+ assert!(device.lock().is_err());253+254+ reset_through_poisoned_lock(device.as_ref(), |device| {255+ device.released = true;256+ Ok(())257+ })258+ .unwrap();259+260+ let device = device.lock().unwrap_err().into_inner();261+ assert!(device.released);262+ }263+264+ 265+ fn content_free_probe_requires_ack_and_never_reaches_uinput() {266+ assert!(ensure_event_was_negotiated(&InputMode::Uinput, &InputEvent::Probe, &[]).is_err());267+ assert!(268+ ensure_event_was_negotiated(269+ &InputMode::Uinput,270+ &InputEvent::Probe,271+ &[Capability::InputAck]272+ )273+ .is_ok()274+ );275+ }276+277+ 278+ fn relative_pointer_events_require_relative_pointer_negotiation() {279+ let movement = InputEvent::MouseMoveRelative { dx: 4, dy: -3 };280+ let synchronization = InputEvent::MousePositionSync { x: 123, y: 456 };281+ let click = InputEvent::MouseClick { b: 1 };282+ let legacy = InputEvent::MouseMove { x: 10, y: 20 };283+ for event in [&movement, &synchronization, &click] {284+ assert!(ensure_event_was_negotiated(&InputMode::Uinput, event, &[]).is_err());285+ assert!(286+ ensure_event_was_negotiated(287+ &InputMode::Uinput,288+ event,289+ &[Capability::AbsolutePointer]290+ )291+ .is_err()292+ );293+ assert!(294+ ensure_event_was_negotiated(295+ &InputMode::Uinput,296+ event,297+ &[Capability::RelativePointer]298+ )299+ .is_ok()300+ );301+ }302+ assert!(303+ ensure_event_was_negotiated(304+ &InputMode::Uinput,305+ &legacy,306+ &[Capability::RelativePointer]307+ )308+ .is_err()309+ );310+ }311+312+ 313+ fn uinput_rejects_unnegotiated_gamepad_snapshots() {314+ let event = InputEvent::Gamepad {315+ state: GamepadState::default(),316+ };317+ assert!(ensure_event_was_negotiated(&InputMode::Uinput, &event, &[]).is_err());318+ assert!(319+ ensure_event_was_negotiated(&InputMode::Uinput, &event, &[Capability::Gamepad]).is_ok()320+ );321+ assert!(ensure_event_was_negotiated(&InputMode::Log, &event, &[]).is_ok());322+ }323+324+ 325+ fn advertised_capabilities_follow_operational_mode() {326+ let disabled = InputBackend {327+ mode: InputMode::Disabled,328+ 329+ device: None,330+ };331+ assert_eq!(disabled.capabilities(), &[Capability::InputAck]);332+333+ let log = InputBackend {334+ mode: InputMode::Log,335+ 336+ device: None,337+ };338+ assert_eq!(log.capabilities(), &[Capability::InputAck]);339+ assert!(!log.capabilities().contains(&Capability::Keyboard));340+ assert_eq!(341+ capabilities_for_mode(&InputMode::Uinput),342+ &[343+ Capability::RelativePointer,344+ Capability::Keyboard,345+ Capability::Gamepad,346+ Capability::InputAck,347+ ]348+ );349+ assert!(!capabilities_for_mode(&InputMode::Uinput).contains(&Capability::AbsolutePointer));350+ }351+}