From 2989f3ef244e516817258ec0d64733fa1b5f3860 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Tue, 16 Dec 2025 22:05:34 -0800 Subject: [PATCH] fix(Profile): use file descriptor instead of profile path BREAKING CHANGE: The LoadProfilePath method has been replaced with LoadProfile. --- .../org.shadowblip.InputPlumber.policy | 6 +- src/cli/device.rs | 6 +- src/cli/ui/menu/device_test_menu.rs | 10 ++- src/config/capability_map.rs | 15 ++-- src/config/config_test.rs | 2 +- src/config/mod.rs | 70 ++++++++++++++++--- src/dbus/interface/composite_device.rs | 57 +++++++-------- src/dbus/interface/manager.rs | 9 ++- src/input/composite_device/client.rs | 13 ++-- src/input/composite_device/command.rs | 13 +++- src/input/composite_device/mod.rs | 32 +++------ src/input/manager.rs | 4 +- 12 files changed, 145 insertions(+), 92 deletions(-) diff --git a/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy b/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy index 60a0da8e..0f531d47 100644 --- a/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy +++ b/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy @@ -113,9 +113,9 @@ - - Load the device profile from the given path - Authorization required to load device profile from a specified path. + + Load the device profile from the given file descriptor + Authorization required to load device profile. no auth_admin diff --git a/src/cli/device.rs b/src/cli/device.rs index 08cd9b9c..1515e469 100644 --- a/src/cli/device.rs +++ b/src/cli/device.rs @@ -1,5 +1,7 @@ use std::error::Error; use std::fmt::Display; +use std::fs::File; +use std::os::fd::OwnedFd; use std::path::PathBuf; use clap::builder::PossibleValuesParser; @@ -232,7 +234,9 @@ pub async fn handle_device( .unwrap_or_default() .to_string_lossy() .to_string(); - if let Err(e) = device.load_profile_path(abs_path).await { + let file = File::open(abs_path)?; + let fd = OwnedFd::from(file); + if let Err(e) = device.load_profile(fd.into()).await { return Err(format!("Failed to load input profile {path}: {e:?}").into()); } println!("Successfully loaded profile: {path}"); diff --git a/src/cli/ui/menu/device_test_menu.rs b/src/cli/ui/menu/device_test_menu.rs index 3744930f..4aee97fe 100644 --- a/src/cli/ui/menu/device_test_menu.rs +++ b/src/cli/ui/menu/device_test_menu.rs @@ -1,4 +1,4 @@ -use std::{error::Error, time::Duration}; +use std::{error::Error, fs::File, os::fd::OwnedFd, time::Duration}; use futures::StreamExt; use packed_struct::PackedStruct; @@ -120,7 +120,9 @@ impl DeviceTestMenu { let profile_dir = get_profiles_path(); let profile_path = profile_dir.join("debug.yaml"); let profile_path = profile_path.to_string_lossy().to_string(); - device.load_profile_path(profile_path).await?; + let profile = File::open(profile_path)?; + let profile_fd = OwnedFd::from(profile); + device.load_profile(profile_fd.into()).await?; } // Create channels to listen for input reports @@ -181,7 +183,9 @@ impl DeviceTestMenu { // Restore the profile if let Some(profile_path) = profile_path { - let _ = device.load_profile_path(profile_path).await; + let profile = File::open(profile_path).unwrap(); + let profile_fd = OwnedFd::from(profile); + let _ = device.load_profile(profile_fd.into()).await; } else if let Some(profile) = profile { let _ = device.load_profile_from_yaml(profile).await; } diff --git a/src/config/capability_map.rs b/src/config/capability_map.rs index e4d0435e..49455a1b 100644 --- a/src/config/capability_map.rs +++ b/src/config/capability_map.rs @@ -1,7 +1,7 @@ pub mod evdev; pub mod hidraw; -use std::{collections::HashMap, io::Read, path::Path}; +use std::{collections::HashMap, fs::File, io::Read, path::Path}; use evdev::EvdevConfig; use hidraw::HidrawConfig; @@ -26,9 +26,8 @@ pub fn load_capability_mappings() -> HashMap { for file in files { // Try to load the capability map log::trace!("Found file: {}", file.display()); - let mapping = CapabilityMapConfig::from_yaml_file(file.display().to_string()); - let map = match mapping { - Ok(map) => map, + let map = match CapabilityMapConfig::from_yaml_path(&file) { + Ok(mapping) => mapping, Err(e) => { log::warn!("Failed to parse capability mapping: {e}",); continue; @@ -61,13 +60,17 @@ impl CapabilityMapConfig { Ok(config) } - /// Load a [CapabilityMapConfig] from the given YAML file - pub fn from_yaml_file

(path: P) -> Result + /// Load a [CapabilityMapConfig] from the given YAML file path + pub fn from_yaml_path

(path: P) -> Result where P: AsRef, { let file = std::fs::File::open(path)?; + Self::from_yaml_file(file) + } + /// Load a [CapabilityMapConfig] from the given YAML file + pub fn from_yaml_file(file: File) -> Result { // Read up to a defined maximum size to prevent denial of service const MAX_SIZE: usize = 512 * 1024; let mut reader = file.take(MAX_SIZE as u64); diff --git a/src/config/config_test.rs b/src/config/config_test.rs index 4be9012c..1a439c5d 100644 --- a/src/config/config_test.rs +++ b/src/config/config_test.rs @@ -32,7 +32,7 @@ async fn check_autostart_rules() -> Result<(), Box> { // Load the config file let path = entry.path(); - let Ok(config) = CompositeDeviceConfig::from_yaml_file(path.display().to_string()) else { + let Ok(config) = CompositeDeviceConfig::from_yaml_path(&path) else { continue; }; diff --git a/src/config/mod.rs b/src/config/mod.rs index 64247766..a4932e94 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,11 +3,17 @@ pub mod capability_map; pub mod config_test; pub mod path; -use std::io::{self, Read}; +use std::{ + fs::File, + io::{self, Read}, + os::fd::OwnedFd, + path::Path, +}; use ::procfs::CpuInfo; use capability_map::CapabilityConfig; use glob_match::glob_match; +use nix::fcntl::{fcntl, FcntlArg, OFlag}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -30,6 +36,8 @@ pub enum LoadError { DeserializeError(#[from] serde_yaml::Error), #[error("Config too large, reached maximum size of {0} bytes")] MaximumSizeReached(usize), + #[error("Operation failed: {0}")] + Os(#[from] nix::errno::Errno), } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -46,16 +54,30 @@ pub struct DeviceProfile { } impl DeviceProfile { - /// Load a [CapabilityProfile] from the given YAML string + /// Load a [DeviceProfile] from the given YAML string pub fn from_yaml(content: String) -> Result { let device: DeviceProfile = serde_yaml::from_str(content.as_str())?; Ok(device) } - /// Load a [CapabilityProfile] from the given YAML file - pub fn from_yaml_file(path: String) -> Result { - let file = std::fs::File::open(path)?; + /// Load a [DeviceProfile] from the given YAML file descriptor + pub fn from_yaml_fd(fd: OwnedFd) -> Result { + validate_fd_flags(&fd)?; + let file = File::from(fd); + Self::from_yaml_file(file) + } + + /// Load a [DeviceProfile] from the given YAML file path + pub fn from_yaml_path

(path: P) -> Result + where + P: AsRef, + { + let file = File::open(path)?; + Self::from_yaml_file(file) + } + /// Load a [DeviceProfile] from the given YAML file + pub fn from_yaml_file(file: File) -> Result { // Read up to a defined maximum size to prevent denial of service const MAX_SIZE: usize = 512 * 1024; let mut reader = file.take(MAX_SIZE as u64); @@ -466,16 +488,30 @@ pub struct CompositeDeviceConfig { } impl CompositeDeviceConfig { - /// Load a [CompositeDevice] from the given YAML string - pub fn from_yaml(content: String) -> Result { + /// Load a [CompositeDeviceConfig] from the given YAML string + pub fn from_yaml(content: String) -> Result { let device: CompositeDeviceConfig = serde_yaml::from_str(content.as_str())?; Ok(device) } - /// Load a [CompositeDevice] from the given YAML file - pub fn from_yaml_file(path: String) -> Result { - let file = std::fs::File::open(path)?; + /// Load a [CompositeDeviceConfig] from the given YAML file descriptor + pub fn from_yaml_fd(fd: OwnedFd) -> Result { + validate_fd_flags(&fd)?; + let file = File::from(fd); + Self::from_yaml_file(file) + } + /// Load a [CompositeDeviceConfig] from the given YAML file path + pub fn from_yaml_path

(path: P) -> Result + where + P: AsRef, + { + let file = File::open(path)?; + Self::from_yaml_file(file) + } + + /// Load a [CompositeDeviceConfig] from the given YAML file + pub fn from_yaml_file(file: File) -> Result { // Read up to a defined maximum size to prevent denial of service const MAX_SIZE: usize = 512 * 1024; let mut reader = file.take(MAX_SIZE as u64); @@ -987,3 +1023,17 @@ impl CompositeDeviceConfig { Some(matches) } } + +/// Ensures the given file descriptor has valid flags set +fn validate_fd_flags(fd: &OwnedFd) -> Result<(), LoadError> { + // Validate the flags for the fd + let flags = fcntl(&fd, FcntlArg::F_GETFL)?; + let Some(flags) = OFlag::from_bits(flags) else { + return Err(LoadError::Os(nix::errno::Errno::EIO)); + }; + if flags.contains(OFlag::O_PATH) { + return Err(LoadError::Os(nix::errno::Errno::EIO)); + } + + Ok(()) +} diff --git a/src/dbus/interface/composite_device.rs b/src/dbus/interface/composite_device.rs index 55730bdf..38a7a602 100644 --- a/src/dbus/interface/composite_device.rs +++ b/src/dbus/interface/composite_device.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -use std::{collections::HashSet, str::FromStr}; +use std::os::fd::AsRawFd; +use std::str::FromStr; +use tokio::fs; +use zbus::zvariant::OwnedFd; use zbus::{ fdo, message::Header, @@ -13,7 +16,7 @@ use crate::{ config::DeviceProfile, dbus::polkit::check_polkit, input::{ - capability::{Capability, Gamepad, Mouse}, + capability::Capability, composite_device::{client::CompositeDeviceClient, InterceptMode}, event::{native::NativeEvent, value::InputValue}, }, @@ -149,21 +152,33 @@ impl CompositeDeviceInterface { Ok(data) } - /// Load the device profile from the given path - async fn load_profile_path( + /// Load the device profile + async fn load_profile( &self, - path: String, + profile: OwnedFd, #[zbus(connection)] conn: &Connection, #[zbus(header)] hdr: Header<'_>, ) -> fdo::Result<()> { check_polkit( conn, Some(hdr), - "org.shadowblip.Input.CompositeDevice.LoadProfilePath", + "org.shadowblip.Input.CompositeDevice.LoadProfile", ) .await?; + + // Try to lookup the path from the file descriptor + let fd = std::os::fd::OwnedFd::from(profile); + let raw_fd = fd.as_raw_fd(); + let proc_path = format!("/proc/self/fd/{raw_fd}"); + let path = fs::read_link(proc_path).await.ok(); + log::debug!("Loading profile: {path:?}"); + + // Load the profile + let profile = + DeviceProfile::from_yaml_fd(fd).map_err(|e| fdo::Error::Failed(e.to_string()))?; + self.composite_device - .load_profile_path(path) + .load_profile(profile, path) .await .map_err(|e| fdo::Error::Failed(e.to_string())) } @@ -462,30 +477,12 @@ impl CompositeDeviceInterface { .get_target_capabilities() .await .map_err(|e| fdo::Error::Failed(e.to_string()))?; + let capability_strings = capabilities + .into_iter() + .map(|cap| cap.to_capability_string()) + .collect(); - let mut capability_strings = HashSet::new(); - for cap in capabilities { - let str = match cap { - Capability::Gamepad(gamepad) => match gamepad { - Gamepad::Button(button) => format!("Gamepad:Button:{}", button), - Gamepad::Axis(axis) => format!("Gamepad:Axis:{}", axis), - Gamepad::Trigger(trigger) => format!("Gamepad:Trigger:{}", trigger), - Gamepad::Accelerometer => "Gamepad:Accelerometer".to_string(), - Gamepad::Gyro => "Gamepad:Gyro".to_string(), - Gamepad::Dial(dial) => format!("Gamepad:Dial:{dial}"), - }, - Capability::Mouse(mouse) => match mouse { - Mouse::Motion => "Mouse:Motion".to_string(), - Mouse::Button(button) => format!("Mouse:Button:{}", button), - }, - Capability::Keyboard(key) => format!("Keyboard:{}", key), - Capability::DBus(action) => format!("DBus:{}", action.as_str()), - _ => cap.to_string(), - }; - capability_strings.insert(str); - } - - Ok(capability_strings.into_iter().collect()) + Ok(capability_strings) } /// List of source devices that this composite device is processing inputs for diff --git a/src/dbus/interface/manager.rs b/src/dbus/interface/manager.rs index bec14da7..3e73f27e 100644 --- a/src/dbus/interface/manager.rs +++ b/src/dbus/interface/manager.rs @@ -1,7 +1,7 @@ use std::time::Duration; use tokio::sync::mpsc; -use zbus::{fdo, message::Header, Connection}; +use zbus::{fdo, message::Header, zvariant::OwnedFd, Connection}; use zbus_macros::interface; use crate::{ @@ -156,11 +156,10 @@ impl ManagerInterface { Ok(supported.iter().map(|id| id.to_string()).collect()) } - /// Create a composite device using the given composite device config. The - /// path should be the absolute path to a composite device configuration file. + /// Create a composite device using the given composite device config. async fn create_composite_device( &self, - config_path: String, + config: OwnedFd, #[zbus(connection)] conn: &Connection, #[zbus(header)] hdr: Header<'_>, ) -> fdo::Result { @@ -170,7 +169,7 @@ impl ManagerInterface { "org.shadowblip.InputPlumber.CreateCompositeDevice", ) .await?; - let device = CompositeDeviceConfig::from_yaml_file(config_path).map_err(|e| match e { + let device = CompositeDeviceConfig::from_yaml_fd(config.into()).map_err(|e| match e { LoadError::IoError(error) => fdo::Error::Failed(error.to_string()), LoadError::MaximumSizeReached(error) => fdo::Error::Failed(error.to_string()), LoadError::DeserializeError(_) => { diff --git a/src/input/composite_device/client.rs b/src/input/composite_device/client.rs index 16c655fb..988359b5 100644 --- a/src/input/composite_device/client.rs +++ b/src/input/composite_device/client.rs @@ -1,11 +1,12 @@ use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use std::time::Duration; use thiserror::Error; use tokio::sync::mpsc::error::SendTimeoutError; use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::{channel, error::SendError, Sender}; -use crate::config::CompositeDeviceConfig; +use crate::config::{CompositeDeviceConfig, DeviceProfile}; use crate::input::event::native::NativeEvent; use crate::input::info::DeviceInfo; use crate::input::output_capability::OutputCapability; @@ -340,11 +341,15 @@ impl CompositeDeviceClient { Err(ClientError::ChannelClosed) } - /// Load the device profile from the given path - pub async fn load_profile_path(&self, path: String) -> Result<(), ClientError> { + /// Load the device profile + pub async fn load_profile( + &self, + profile: DeviceProfile, + path: Option, + ) -> Result<(), ClientError> { let (tx, rx) = channel(1); self.tx - .send(CompositeCommand::LoadProfilePath(path, tx)) + .send(CompositeCommand::LoadProfile(profile, path, tx)) .await?; if let Some(result) = Self::recv(rx).await { return match result { diff --git a/src/input/composite_device/command.rs b/src/input/composite_device/command.rs index a3267841..af61aa40 100644 --- a/src/input/composite_device/command.rs +++ b/src/input/composite_device/command.rs @@ -1,9 +1,12 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; use tokio::sync::mpsc; use crate::{ - config::CompositeDeviceConfig, + config::{CompositeDeviceConfig, DeviceProfile}, input::{ capability::Capability, event::{native::NativeEvent, Event}, @@ -36,7 +39,11 @@ pub enum CompositeCommand { GetTargetDevicePaths(mpsc::Sender>), HandleEvent(NativeEvent), LoadProfileFromYaml(String, mpsc::Sender>), - LoadProfilePath(String, mpsc::Sender>), + LoadProfile( + DeviceProfile, + Option, + mpsc::Sender>, + ), ProcessEvent(String, Event), ProcessOutputEvent(OutputEvent), RemoveRecentEvent(Capability), diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 7e2a8920..879eafcb 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -19,7 +19,7 @@ use zbus::{object_server::Interface, Connection}; use crate::{ config::{ capability_map::CapabilityMapConfig, path::get_profiles_path, CompositeDeviceConfig, - DeviceProfile, LoadError, ProfileMapping, + DeviceProfile, ProfileMapping, }, dbus::interface::{ composite_device::CompositeDeviceInterface, force_feedback::ForceFeedbackInterface, @@ -227,7 +227,7 @@ impl CompositeDevice { let profile_dir = get_profiles_path(); let profile_path = profile_dir.join("default.yaml"); let profile_path = profile_path.to_string_lossy().to_string(); - let profile = DeviceProfile::from_yaml_file(profile_path.clone())?; + let profile = DeviceProfile::from_yaml_path(profile_path.clone())?; device.load_device_profile(Some(profile), Some(profile_path))?; // If a capability map is defined, add those target capabilities to @@ -433,27 +433,11 @@ impl CompositeDevice { log::error!("Failed to send load profile result: {:?}", e); } } - CompositeCommand::LoadProfilePath(path, sender) => { - log::debug!("Loading profile from path: {path}"); - let profile = match DeviceProfile::from_yaml_file(path.clone()) { - Ok(p) => p, - Err(e) => { - let err = match e { - LoadError::IoError(_) => e.to_string(), - LoadError::MaximumSizeReached(_) => e.to_string(), - LoadError::DeserializeError(_) => { - "Failed to parse file".to_string() - } - }; - if let Err(er) = sender.send(Err(err)).await { - log::error!("Failed to send failed to load profile: {er:?}"); - } - continue; - } - }; - let result = match self - .load_device_profile(Some(profile.clone()), Some(path.clone())) - { + CompositeCommand::LoadProfile(profile, path, sender) => { + let result = match self.load_device_profile( + Some(profile.clone()), + path.clone().map(|p| p.display().to_string()), + ) { Ok(_) => Ok(()), Err(e) => Err(e.to_string()), }; @@ -461,7 +445,7 @@ impl CompositeDevice { self.dbus.connection(), self.dbus.path(), Some(profile), - Some(path), + path.map(|p| p.display().to_string()), ); if let Err(e) = sender.send(result).await { log::error!("Failed to send load profile result: {:?}", e); diff --git a/src/input/manager.rs b/src/input/manager.rs index fe31b041..51f36dea 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -299,7 +299,7 @@ impl Manager { let path = device.keys().next().cloned(); let response = match path { Some(path) => Ok(path), - _ => Err(ManagerError::CreateTargetDeviceFailed( + None => Err(ManagerError::CreateTargetDeviceFailed( "Unable to find device path".to_string(), )), }; @@ -1657,7 +1657,7 @@ impl Manager { for file in files { // Try to load the composite device profile log::trace!("Found file: {}", file.display()); - let device = CompositeDeviceConfig::from_yaml_file(file.display().to_string()); + let device = CompositeDeviceConfig::from_yaml_path(&file); let device = match device { Ok(dev) => dev, Err(e) => {