From 5163bdc3673100d3243de5097c46700f0a313a1c1d9ac1a9e74796c83d4a1159 Mon Sep 17 00:00:00 2001 From: sisungo Date: Tue, 1 Sep 2026 13:01:13 +0000 Subject: [PATCH] Initial commit Signed-off-by: sisungo --- .gitignore | 2 + Cargo.toml | 3 + LICENSE | 24 + cli/Cargo.toml | 9 + cli/src/main.rs | 18 + cli/src/trigger.rs | 17 + daemon/Cargo.toml | 17 + daemon/src/device.rs | 209 +++++++ daemon/src/ipc.rs | 67 ++ daemon/src/main.rs | 25 + daemon/src/rules.rs | 42 ++ daemon/src/trigger.rs | 62 ++ daemon/src/uevent.rs | 22 + daemon/src/util.rs | 88 +++ libcompat/udev/Cargo.toml | 10 + libcompat/udev/include/libudev.h | 124 ++++ libcompat/udev/src/lib.rs | 901 +++++++++++++++++++++++++++ libs/ipc/Cargo.toml | 11 + libs/ipc/src/client.rs | 63 ++ libs/ipc/src/lib.rs | 5 + libs/ipc/src/repr.rs | 64 ++ libs/ipc/src/server.rs | 61 ++ libs/kobject-uevent/Cargo.toml | 8 + libs/kobject-uevent/src/lib.rs | 87 +++ libs/rule/Cargo.toml | 14 + libs/rule/src/engine.rs | 359 +++++++++++ libs/rule/src/error.rs | 20 + libs/rule/src/lib.rs | 5 + libs/rule/src/parser.rs | 124 ++++ misc/airup/lxdeviced.airs | 19 + misc/rules/0010-basic-pseudo-dev.ron | 66 ++ misc/rules/0015-kmod.ron | 8 + misc/rules/0020-tty.ron | 17 + misc/rules/0050-blockdev.ron | 54 ++ 34 files changed, 2625 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 cli/Cargo.toml create mode 100644 cli/src/main.rs create mode 100644 cli/src/trigger.rs create mode 100644 daemon/Cargo.toml create mode 100644 daemon/src/device.rs create mode 100644 daemon/src/ipc.rs create mode 100644 daemon/src/main.rs create mode 100644 daemon/src/rules.rs create mode 100644 daemon/src/trigger.rs create mode 100644 daemon/src/uevent.rs create mode 100644 daemon/src/util.rs create mode 100644 libcompat/udev/Cargo.toml create mode 100644 libcompat/udev/include/libudev.h create mode 100644 libcompat/udev/src/lib.rs create mode 100644 libs/ipc/Cargo.toml create mode 100644 libs/ipc/src/client.rs create mode 100644 libs/ipc/src/lib.rs create mode 100644 libs/ipc/src/repr.rs create mode 100644 libs/ipc/src/server.rs create mode 100644 libs/kobject-uevent/Cargo.toml create mode 100644 libs/kobject-uevent/src/lib.rs create mode 100644 libs/rule/Cargo.toml create mode 100644 libs/rule/src/engine.rs create mode 100644 libs/rule/src/error.rs create mode 100644 libs/rule/src/lib.rs create mode 100644 libs/rule/src/parser.rs create mode 100644 misc/airup/lxdeviced.airs create mode 100644 misc/rules/0010-basic-pseudo-dev.ron create mode 100644 misc/rules/0015-kmod.ron create mode 100644 misc/rules/0020-tty.ron create mode 100644 misc/rules/0050-blockdev.ron diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a320d84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7c91d37 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["cli", "daemon", "libcompat/udev", "libs/ipc", "libs/kobject-uevent", "libs/rule"] +resolver = "3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cc5924 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..3b267c8 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "device" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +ipc = { path = "../libs/ipc" } diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..ba79b77 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,18 @@ +mod trigger; + +use clap::Parser; + +#[derive(Debug, Parser)] +enum Cli { + Trigger(trigger::Cli), +} + +fn main() { + let cli = Cli::parse(); + let result = match cli { + Cli::Trigger(cli) => trigger::main(cli), + }; + if let Err(err) = result { + eprintln!("device: error: {err}"); + } +} diff --git a/cli/src/trigger.rs b/cli/src/trigger.rs new file mode 100644 index 0000000..2b09e81 --- /dev/null +++ b/cli/src/trigger.rs @@ -0,0 +1,17 @@ +use clap::Parser; +use ipc::client::Client; + +#[derive(Debug, Parser)] +pub struct Cli { + #[arg(short, long, default_value = "all")] + set: String, + + #[arg(short, long, default_value = "change")] + action: String, +} + +pub fn main(cli: Cli) -> anyhow::Result<()> { + let mut client = Client::connect()?; + client.trigger(cli.set, cli.action)?; + Ok(()) +} diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml new file mode 100644 index 0000000..668dd28 --- /dev/null +++ b/daemon/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "lxdeviced" +version = "0.1.0" +edition = "2024" + +[dependencies] +ipc = { path = "../libs/ipc" } +kobject-uevent = { path = "../libs/kobject-uevent" } +libc = "0.2" +nix = { version = "0.31", features = ["fs", "user"] } +rule = { path = "../libs/rule" } +rustc-hash = "2" +serde = "1" +serde_json = "1" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "fs", "signal", "sync"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/daemon/src/device.rs b/daemon/src/device.rs new file mode 100644 index 0000000..166894a --- /dev/null +++ b/daemon/src/device.rs @@ -0,0 +1,209 @@ +use kobject_uevent::KobjectUevent; +use rule::{engine::Engine, parser::When}; +use rustc_hash::FxHashMap; +use std::{ + fs::Permissions, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, + sync::{Arc, LazyLock, RwLock}, +}; +use tokio::sync::Mutex; + +static DEVICES: LazyLock>> = + LazyLock::new(|| Default::default()); + +pub type ArcDevice = Arc; + +#[derive(Debug)] +pub struct Device { + lock: Mutex<()>, + engine: Engine, +} +impl Device { + pub fn new() -> Self { + let engine = Engine::new(); + engine.set_property("DEVROOT".into(), dev_root().to_string_lossy().into()); + + Self { + lock: Mutex::new(()), + engine, + } + } + + fn apply_uevent(&self, ev: &KobjectUevent) { + for (k, v) in ev.inner().iter() { + self.engine.set_property(k.into(), v.into()); + } + self.update_derived_properties(); + } + + fn update_derived_properties(&self) { + if let Some(devname) = self.engine.get_property("DEVNAME") { + self.engine.set_property( + "DEVNODE".into(), + format!("{}/{devname}", dev_root().display()), + ); + } + } + + async fn update_devnode_creds(&self) { + let Some(devnode) = self.engine.get_property("DEVNODE") else { + return; + }; + let uid = crate::util::uid_by_string(self.engine.get_property("OWNER").as_deref()); + let gid = crate::util::gid_by_string(self.engine.get_property("GROUP").as_deref()); + let mode = crate::util::parse_mode(self.engine.get_property("MODE").as_deref()); + + if let Err(err) = crate::util::set_ownership(&devnode, uid, gid).await { + tracing::warn!("{devnode}: failed to set ownership: {err}"); + } + + if let Some(mode) = mode + && let Err(err) = + tokio::fs::set_permissions(&devnode, Permissions::from_mode(mode as _)).await + { + tracing::warn!("{devnode}: failed to set permissions: {err}"); + } + } + + async fn update_symlinks(&self) { + let Some(devnode) = self.engine.get_property("DEVNODE") else { + return; + }; + for i in self.engine.get_list("SYMLINKS") { + if let Ok(x) = tokio::fs::read_link(&i).await + && x == Path::new(&devnode) + { + return; + } + if let Some(parent) = Path::new(&i).parent() { + _ = tokio::fs::create_dir_all(parent).await; + } + if let Err(err) = tokio::fs::symlink(&devnode, &i).await { + tracing::warn!("{devnode}: failed to create symbolic link \"{i}\": {err}"); + } + } + } + + async fn remove_symlinks(&self) { + for i in self.engine.get_list("SYMLINKS") { + if let Err(err) = tokio::fs::remove_file(&i).await { + tracing::warn!("failed to remove symbolic link \"{i}\": {err}"); + } + } + } +} + +pub async fn handle_uevent(ev: &KobjectUevent) { + match ev.action() { + Some("add") => add(ev).await, + Some("remove") => remove(ev).await, + Some("change") => change(ev).await, + Some(other) => { + tracing::warn!("Unrecognized uevent action \"{other}\", should we update lxdeviced?"); + } + None => (), + } +} + +async fn add(ev: &KobjectUevent) { + assert_eq!(ev.action(), Some("add")); + + let Some(devpath) = ev.devpath() else { + return; + }; + let device = Device::new(); + device.apply_uevent(&ev); + device.engine.exec(&crate::rules::get()).await; + + device.update_devnode_creds().await; + device.update_symlinks().await; + + DEVICES + .write() + .unwrap() + .insert(devpath.into(), Arc::new(device)); +} + +async fn remove(ev: &KobjectUevent) { + assert_eq!(ev.action(), Some("remove")); + + let Some(devpath) = ev.devpath() else { + return; + }; + let Some(device) = DEVICES.read().unwrap().get(devpath).cloned() else { + return; + }; + let _guard = device.lock.lock().await; + device.apply_uevent(ev); + device.engine.exec(&crate::rules::get()).await; + + device.remove_symlinks().await; + + DEVICES.write().unwrap().remove(devpath); +} + +async fn change(ev: &KobjectUevent) { + assert_eq!(ev.action(), Some("change")); + + let Some(devpath) = ev.devpath() else { + return; + }; + let Some(device) = DEVICES.read().unwrap().get(devpath).cloned() else { + return; + }; + let _guard = device.lock.lock().await; + device.apply_uevent(ev); + device.engine.exec(&crate::rules::get()).await; +} + +pub async fn update_devnode(ev: &KobjectUevent) { + if std::env::var("LXDEVICED_HAS_DEVTMPFS").as_deref() == Ok("1") { + return; + } + let Some(devname) = ev.devname() else { + return; + }; + let devnode = dev_root().join(devname); + if ev.action() == Some("add") { + if let Some(parent) = devnode.parent() { + _ = tokio::fs::create_dir_all(&parent).await; + } + let Some(major) = ev.major() else { + return; + }; + let Some(minor) = ev.minor() else { + return; + }; + let mode = ev.devmode().unwrap_or(0o600); + let dev = libc::makedev(major, minor); + if ev.subsystem() == Some("block") { + _ = crate::util::create_blockdev(devnode, dev, mode).await; + } else { + _ = crate::util::create_chardev(devnode, dev, mode).await; + } + } else if ev.action() == Some("remove") { + _ = tokio::fs::remove_file(&devnode).await; + } +} + +pub async fn list_devices(when: When) -> Vec { + let mut ret = Vec::new(); + let keys: Vec = DEVICES.read().unwrap().keys().map(Into::into).collect(); + for key in keys { + let Some(device) = DEVICES.read().unwrap().get(&key).cloned() else { + continue; + }; + if device.engine.exec_condition(&when).unwrap_or_default() { + ret.push(key); + } + } + ret +} + +fn dev_root() -> PathBuf { + std::env::var("LXDEVICED_DEV_ROOT") + .as_deref() + .unwrap_or("/dev") + .into() +} diff --git a/daemon/src/ipc.rs b/daemon/src/ipc.rs new file mode 100644 index 0000000..b8f151a --- /dev/null +++ b/daemon/src/ipc.rs @@ -0,0 +1,67 @@ +use ipc::{ + repr::{Error, Request}, + server::{Connection, Listener}, +}; +use rule::parser::When; +use serde::Serialize; +use std::pin::Pin; + +pub fn start_server() -> std::io::Result<()> { + let listener = Listener::bind()?; + tokio::spawn(server(listener)); + Ok(()) +} + +async fn server(listener: Listener) { + loop { + let Ok(connection) = listener.accept().await else { + continue; + }; + tracing::debug!( + "Established connection with {{ uid={}, gid={}, pid={} }}", + connection.uid, + connection.gid, + connection.pid + ); + tokio::spawn(handle_connection(connection)); + } +} + +async fn handle_connection(mut connection: Connection) -> std::io::Result<()> { + loop { + let request = connection.recv().await?; + let handler = match request { + Request::Trigger(set, action) => handler(trigger(set, action)), + Request::ReloadRules => handler(reload_rules()), + Request::ListDevices(when) => handler(list_devices(when)), + }; + let reply = handler.await; + connection.send(reply).await?; + } +} + +async fn trigger(set: String, action: String) -> Result<(), Error> { + if !crate::trigger::VALID_ACTIONS.contains(&action.as_str()) { + return Err(Error::BadRequest); + } + crate::trigger::trigger(&set, &action).await; + Ok(()) +} + +async fn reload_rules() -> Result<(), Error> { + crate::rules::reload(); + Ok(()) +} + +async fn list_devices(when: When) -> Result, Error> { + Ok(crate::device::list_devices(when).await) +} + +fn handler( + fut: impl Future> + Send + 'static, +) -> Pin> + Send>> { + Box::pin(async move { + let reply = fut.await; + reply.map(|x| serde_json::to_value(x).unwrap()) + }) +} diff --git a/daemon/src/main.rs b/daemon/src/main.rs new file mode 100644 index 0000000..f4403ef --- /dev/null +++ b/daemon/src/main.rs @@ -0,0 +1,25 @@ +mod device; +mod ipc; +mod rules; +mod trigger; +mod uevent; +mod util; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + if let Err(err) = ipc::start_server() { + tracing::error!("Failed to start IPC server: {err}"); + std::process::exit(1); + } + + if let Err(err) = uevent::start_monitor() { + tracing::error!("Failed to start KOBJECT_UEVENT monitor: {err}"); + std::process::exit(1); + } + + rules::reload(); + + _ = tokio::signal::ctrl_c().await; +} diff --git a/daemon/src/rules.rs b/daemon/src/rules.rs new file mode 100644 index 0000000..d68430c --- /dev/null +++ b/daemon/src/rules.rs @@ -0,0 +1,42 @@ +use rule::engine::RuleSet; +use std::{ + path::PathBuf, + sync::{LazyLock, RwLock}, +}; + +static RULE_SET: LazyLock> = LazyLock::new(|| RwLock::new(RuleSet::empty())); + +pub fn reload() { + *RULE_SET.write().unwrap() = RuleSet::open_dirs(rule_dirs().iter()); +} + +pub fn get() -> RuleSet { + RULE_SET.read().unwrap().clone() +} + +pub fn rule_dirs() -> Vec { + let mut all = Vec::with_capacity(1); + if let Some(path) = bundle_rule_dir() { + all.push(path); + } + if let Ok(val) = std::env::var("LXDEVICED_EXTRA_RULE_DIRS") { + for path in val.split(':') { + all.push(path.into()); + } + } + all.push("/share/device/rules".into()); + all.push("/usr/share/device/rules".into()); + all.push("/etc/device/rules".into()); + all.push("/var/config/device/rules".into()); + all +} + +fn bundle_rule_dir() -> Option { + // current_exe: "/bundle/bin/lxdeviced" + std::fs::canonicalize(std::env::current_exe().ok()?) + .ok()? + .parent()? + .parent()? + .join("share/device/rules") + .into() +} diff --git a/daemon/src/trigger.rs b/daemon/src/trigger.rs new file mode 100644 index 0000000..4f7b96e --- /dev/null +++ b/daemon/src/trigger.rs @@ -0,0 +1,62 @@ +use std::path::{Path, PathBuf}; + +pub const VALID_ACTIONS: &[&str] = &["add", "remove", "change"]; + +pub async fn trigger(set: &str, action: &str) { + assert!(VALID_ACTIONS.contains(&action)); + for i in set_to_dirs(set) { + let path = &sysfs_root().join(i); + if trigger_dir(path, action).await.is_err() { + tracing::warn!( + "\"{}\" is inaccessible. Check your system configuration.", + path.display() + ); + } + } +} + +async fn trigger_dir(path: &Path, action: &str) -> std::io::Result<()> { + let mut read_dir = tokio::fs::read_dir(path).await?; + while let Ok(Some(dirent)) = read_dir.next_entry().await { + if dirent.file_name().as_encoded_bytes() == b"uevent" { + match tokio::fs::write(dirent.path(), action.as_bytes()).await { + Ok(()) => { + tracing::debug!( + "Triggering uevent \"{action}\" for \"{}\".", + dirent.path().display() + ); + } + Err(err) => { + tracing::warn!( + "Failed to trigger uevent \"{action}\" for \"{}\": {err}", + dirent.path().display() + ); + } + }; + } else if dirent + .file_type() + .await + .map(|x| x.is_dir()) + .unwrap_or_default() + { + _ = Box::pin(trigger_dir(&dirent.path(), action)).await; + } + } + Ok(()) +} + +fn set_to_dirs(set: &str) -> Vec { + if set == "all" { + vec![sysfs_root().into()] + } else if set == "devices" { + vec![sysfs_root().join("devices")] + } else { + vec![] + } +} + +fn sysfs_root() -> PathBuf { + std::env::var("LXDEVICED_SYSFS_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|_| "/sys".into()) +} diff --git a/daemon/src/uevent.rs b/daemon/src/uevent.rs new file mode 100644 index 0000000..bd33e05 --- /dev/null +++ b/daemon/src/uevent.rs @@ -0,0 +1,22 @@ +use kobject_uevent::KobjectUeventSocket; + +pub fn start_monitor() -> std::io::Result<()> { + let socket = KobjectUeventSocket::connect(1)?; + tokio::spawn(async move { + loop { + let ev = match socket.recv().await { + Ok(ev) => ev, + Err(err) => { + tracing::warn!("Failed to receive KOBJECT_UEVENT: {err}"); + continue; + } + }; + tracing::debug!("Received KOBJECT_UEVENT: {ev:?}"); + crate::device::update_devnode(&ev).await; + tokio::spawn(async move { + crate::device::handle_uevent(&ev).await; + }); + } + }); + Ok(()) +} diff --git a/daemon/src/util.rs b/daemon/src/util.rs new file mode 100644 index 0000000..b5fde99 --- /dev/null +++ b/daemon/src/util.rs @@ -0,0 +1,88 @@ +use nix::{ + sys::stat::{Mode, SFlag}, + unistd::{Group, User}, +}; +use std::{os::unix::fs::lchown, path::PathBuf}; + +pub async fn create_blockdev(path: impl Into, dev: u64, mode: u16) -> std::io::Result<()> { + create_nodefile(path, SFlag::S_IFBLK, dev, mode).await +} + +pub async fn create_chardev(path: impl Into, dev: u64, mode: u16) -> std::io::Result<()> { + create_nodefile(path, SFlag::S_IFCHR, dev, mode).await +} + +pub async fn set_ownership( + path: impl Into, + uid: Option, + gid: Option, +) -> std::io::Result<()> { + let path = path.into(); + tokio::task::spawn_blocking(move || lchown(&path, uid, gid)) + .await + .unwrap() + .map_err(Into::into) +} + +pub fn uid_by_string(user: Option<&str>) -> Option { + let mut uid = user.and_then(|x| { + x.parse::().ok().or_else(|| { + User::from_name(x) + .ok() + .flatten() + .map(|user| user.uid.as_raw()) + }) + }); + if uid.is_none() + && let Some(user) = user + { + uid = Some(0); + tracing::warn!("Unrecognized user \"{user}\", fallbacking to root",); + } + uid +} + +pub fn gid_by_string(group: Option<&str>) -> Option { + let mut gid = group.and_then(|x| { + x.parse::().ok().or_else(|| { + Group::from_name(x) + .ok() + .flatten() + .map(|group| group.gid.as_raw()) + }) + }); + if gid.is_none() + && let Some(group) = group + { + gid = Some(0); + tracing::warn!("Unrecognized group \"{group}\", fallbacking to root",); + } + gid +} + +pub fn parse_mode(mode_raw: Option<&str>) -> Option { + let mode = mode_raw + .and_then(|x| x.strip_prefix("0o")) + .and_then(|x| u16::from_str_radix(x, 8).ok()); + if mode.is_none() + && let Some(mode_raw) = mode_raw + { + tracing::warn!("Invalid mode \"{mode_raw}\", fallbacking to the default value"); + } + mode +} + +async fn create_nodefile( + path: impl Into, + sflag: SFlag, + dev: u64, + mode: u16, +) -> std::io::Result<()> { + let path = path.into(); + tokio::task::spawn_blocking(move || { + nix::sys::stat::mknod(&path, sflag, Mode::from_bits_retain(mode as _), dev) + }) + .await + .unwrap() + .map_err(Into::into) +} diff --git a/libcompat/udev/Cargo.toml b/libcompat/udev/Cargo.toml new file mode 100644 index 0000000..816bae0 --- /dev/null +++ b/libcompat/udev/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "udev" +version = "0.1.0" +edition = "2024" + +[dependencies] +ipc = { path = "../../libs/ipc" } +libc = "0.2" +rustc-hash = "2" +rule = { path = "../../libs/rule" } diff --git a/libcompat/udev/include/libudev.h b/libcompat/udev/include/libudev.h new file mode 100644 index 0000000..5356cf5 --- /dev/null +++ b/libcompat/udev/include/libudev.h @@ -0,0 +1,124 @@ +#ifndef _LIBUDEV_H_ +#define _LIBUDEV_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct udev; +struct udev* udev_ref(struct udev *p); +struct udev* udev_unref(struct udev *udev); +struct udev* udev_new(void); +void* udev_get_userdata(struct udev *udev); +void udev_set_userdata(struct udev *udev, void *userdata); + +struct udev_list_entry; +struct udev_list_entry* udev_list_entry_get_next(struct udev_list_entry *list_entry); +struct udev_list_entry* udev_list_entry_get_by_name(struct udev_list_entry *list_entry, const char *name); +const char* udev_list_entry_get_name(struct udev_list_entry *list_entry); +const char* udev_list_entry_get_value(struct udev_list_entry *list_entry); +#define udev_list_entry_foreach(list_entry, first_entry) \ + for (list_entry = first_entry; \ + list_entry; \ + list_entry = udev_list_entry_get_next(list_entry)) + +struct udev_device; +struct udev_device* udev_device_ref(struct udev_device *p); +struct udev_device* udev_device_unref(struct udev_device *p); +struct udev* udev_device_get_udev(struct udev_device *udev_device); +struct udev_device* udev_device_new_from_syspath(struct udev *udev, const char *syspath); +struct udev_device* udev_device_new_from_devnum(struct udev *udev, char type, dev_t devnum); +struct udev_device* udev_device_new_from_subsystem_sysname(struct udev *udev, const char *subsystem, const char *sysname); +struct udev_device* udev_device_new_from_device_id(struct udev *udev, const char *id); +struct udev_device* udev_device_new_from_environment(struct udev *udev); +struct udev_device* udev_device_get_parent(struct udev_device *udev_device); +struct udev_device* udev_device_get_parent_with_subsystem_devtype( + struct udev_device *udev_device, + const char *subsystem, + const char *devtype); +const char* udev_device_get_devpath(struct udev_device *udev_device); +const char* udev_device_get_subsystem(struct udev_device *udev_device); +const char* udev_device_get_devtype(struct udev_device *udev_device); +const char* udev_device_get_syspath(struct udev_device *udev_device); +const char* udev_device_get_sysname(struct udev_device *udev_device); +const char* udev_device_get_sysnum(struct udev_device *udev_device); +const char* udev_device_get_devnode(struct udev_device *udev_device); +int udev_device_get_is_initialized(struct udev_device *udev_device); +struct udev_list_entry* udev_device_get_devlinks_list_entry(struct udev_device *udev_device); +struct udev_list_entry* udev_device_get_properties_list_entry(struct udev_device *udev_device); +struct udev_list_entry* udev_device_get_tags_list_entry(struct udev_device *udev_device); +struct udev_list_entry* udev_device_get_current_tags_list_entry(struct udev_device *udev_device); +struct udev_list_entry* udev_device_get_sysattr_list_entry(struct udev_device *udev_device); +const char* udev_device_get_property_value(struct udev_device *udev_device, const char *key); +const char* udev_device_get_driver(struct udev_device *udev_device); +dev_t udev_device_get_devnum(struct udev_device *udev_device); +const char* udev_device_get_action(struct udev_device *udev_device); +unsigned long long int udev_device_get_seqnum(struct udev_device *udev_device); +unsigned long long int udev_device_get_usec_since_initialized(struct udev_device *udev_device); +const char* udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr); +int udev_device_set_sysattr_value(struct udev_device *udev_device, const char *sysattr, const char *value); +int udev_device_has_tag(struct udev_device *udev_device, const char *tag); +int udev_device_has_current_tag(struct udev_device *udev_device, const char *tag); + +struct udev_monitor; +struct udev_monitor* udev_monitor_ref(struct udev_monitor *p); +struct udev_monitor* udev_monitor_unref(struct udev_monitor *p); +struct udev* udev_monitor_get_udev(struct udev_monitor *udev_monitor); +struct udev_monitor* udev_monitor_new_from_netlink(struct udev *udev, const char *name); +int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor); +int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size); +int udev_monitor_get_fd(struct udev_monitor *udev_monitor); +struct udev_device* udev_monitor_receive_device(struct udev_monitor *udev_monitor); +int udev_monitor_filter_add_match_subsystem_devtype( + struct udev_monitor *udev_monitor, + const char *subsystem, + const char *devtype); +int udev_monitor_filter_add_match_tag(struct udev_monitor *udev_monitor, const char *tag); +int udev_monitor_filter_update(struct udev_monitor *udev_monitor); +int udev_monitor_filter_remove(struct udev_monitor *udev_monitor); + +struct udev_enumerate; +struct udev_enumerate* udev_enumerate_ref(struct udev_enumerate *p); +struct udev_enumerate* udev_enumerate_unref(struct udev_enumerate *p); +struct udev* udev_enumerate_get_udev(struct udev_enumerate *udev_enumerate); +struct udev_enumerate* udev_enumerate_new(struct udev *udev); +int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem); +int udev_enumerate_add_nomatch_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem); +int udev_enumerate_add_match_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value); +int udev_enumerate_add_nomatch_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value); +int udev_enumerate_add_match_property(struct udev_enumerate *udev_enumerate, const char *property, const char *value); +int udev_enumerate_add_match_sysname(struct udev_enumerate *udev_enumerate, const char *sysname); +int udev_enumerate_add_match_tag(struct udev_enumerate *udev_enumerate, const char *tag); +int udev_enumerate_add_match_parent(struct udev_enumerate *udev_enumerate, struct udev_device *parent); +int udev_enumerate_add_match_is_initialized(struct udev_enumerate *udev_enumerate); +int udev_enumerate_add_syspath(struct udev_enumerate *udev_enumerate, const char *syspath); +int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate); +int udev_enumerate_scan_subsystems(struct udev_enumerate *udev_enumerate); +struct udev_list_entry* udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate); + +struct udev_queue; +struct udev_queue* udev_queue_ref(struct udev_queue *p); +struct udev_queue* udev_queue_unref(struct udev_queue *p); +struct udev* udev_queue_get_udev(struct udev_queue *udev_queue); +struct udev_queue* udev_queue_new(struct udev *udev); +int udev_queue_get_udev_is_active(struct udev_queue *udev_queue); +int udev_queue_get_queue_is_empty(struct udev_queue *udev_queue); +int udev_queue_get_fd(struct udev_queue *udev_queue); +int udev_queue_flush(struct udev_queue *udev_queue); + +struct udev_hwdb; +struct udev_hwdb* udev_hwdb_new(struct udev *udev); +struct udev_hwdb* udev_hwdb_ref(struct udev_hwdb *p); +struct udev_hwdb* udev_hwdb_unref(struct udev_hwdb *p); +struct udev_list_entry* udev_hwdb_get_properties_list_entry(struct udev_hwdb *hwdb, const char *modalias, unsigned flags); + +int udev_util_encode_string(const char *str, char *str_enc, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libcompat/udev/src/lib.rs b/libcompat/udev/src/lib.rs new file mode 100644 index 0000000..351ef00 --- /dev/null +++ b/libcompat/udev/src/lib.rs @@ -0,0 +1,901 @@ +use ipc::client::Client; +use rule::parser::{Str, When}; +use rustc_hash::FxHashMap; +use std::{ + ffi::{CStr, CString}, + str::FromStr, + sync::atomic::{self, AtomicUsize}, +}; + +#[derive(Debug)] +pub struct Udev { + client: Client, + userdata: *mut u8, + refcount: AtomicUsize, +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_new() -> *mut Udev { + let Ok(client) = Client::connect() else { + return std::ptr::null_mut(); + }; + Box::into_raw(Box::new(Udev { + client, + userdata: std::ptr::null_mut(), + refcount: AtomicUsize::new(1), + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_ref(udev: *mut Udev) -> *mut Udev { + unsafe { + (*udev).refcount.fetch_add(1, atomic::Ordering::SeqCst); + } + udev +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_unref(udev: *mut Udev) -> *mut Udev { + unsafe { + let oldval = (*udev).refcount.fetch_sub(1, atomic::Ordering::SeqCst); + if oldval == 1 { + drop(Box::from_raw(udev)); + std::ptr::null_mut() + } else { + udev + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_get_userdata(udev: *const Udev) -> *mut u8 { + unsafe { (*udev).userdata } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_set_userdata(udev: *mut Udev, userdata: *mut u8) { + unsafe { + (*udev).userdata = userdata; + } +} + +#[derive(Debug)] +pub struct UdevListEntry { + key: CString, + value: CString, + next: Option>, +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_list_entry_get_next( + entry: *const UdevListEntry, +) -> *const UdevListEntry { + unsafe { + (*entry) + .next + .as_ref() + .map(|x| &raw const **x) + .unwrap_or_default() + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_list_entry_get_by_name( + mut entry: *const UdevListEntry, + name: *const libc::c_char, +) -> *const UdevListEntry { + unsafe { + let name = CStr::from_ptr(name); + while !entry.is_null() { + if (*entry).key == name { + return entry; + } + entry = udev_list_entry_get_next(entry); + } + std::ptr::null() + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_list_entry_get_name( + entry: *const UdevListEntry, +) -> *const libc::c_char { + unsafe { (*entry).key.as_ptr() } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_list_entry_get_value( + entry: *const UdevListEntry, +) -> *const libc::c_char { + unsafe { (*entry).value.as_ptr() } +} + +#[derive(Debug)] +pub struct UdevDevice { + refcount: AtomicUsize, + udev: *mut Udev, + properties: UdevListEntry, + syspath: Option, + devlinks: UdevListEntry, + tags: UdevListEntry, + sysattr: UdevListEntry, + action: Option, + seqnum: libc::c_ulonglong, + sysattr_cache: FxHashMap, +} +impl UdevDevice { + fn new(udev: *mut Udev, properties: FxHashMap) -> Self { + let syspath_str = properties.get("DEVPATH").map(|x| format!("/sys/{x}")); + let syspath = syspath_str + .as_deref() + .map(|x| CString::from_str(x).unwrap_or_default()); + let devlinks = properties + .get("SYMLINKS") + .map(|x| x.as_str()) + .unwrap_or_default() + .split('\0') + .filter(|x| !x.is_empty()) + .map(|x| x.to_string()) + .collect(); + let devlinks = vec_to_list(&devlinks); + let tags = properties + .get("TAGS") + .map(|x| x.as_str()) + .unwrap_or_default() + .split('\0') + .filter(|x| !x.is_empty()) + .map(|x| x.to_string()) + .collect(); + let tags = vec_to_list(&tags); + let mut sysattr = Vec::new(); + if let Some(syspath_str) = &syspath_str + && let Ok(read_dir) = std::fs::read_dir(syspath_str) + { + for i in read_dir { + let Ok(i) = i else { + break; + }; + sysattr.push(i.file_name().to_string_lossy().to_string()); + } + } + let sysattr = vec_to_list(&sysattr); + Self { + refcount: AtomicUsize::new(1), + udev, + properties: *hashmap_to_list(&properties), + syspath, + devlinks: *devlinks, + tags: *tags, + sysattr: *sysattr, + action: None, + seqnum: 0, + sysattr_cache: Default::default(), + } + } +} +impl Drop for UdevDevice { + fn drop(&mut self) { + unsafe { + udev_unref(self.udev); + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_ref(dev: *mut UdevDevice) -> *mut UdevDevice { + unsafe { + (*dev).refcount.fetch_add(1, atomic::Ordering::SeqCst); + } + dev +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_unref(dev: *mut UdevDevice) -> *mut UdevDevice { + unsafe { + let oldval = (*dev).refcount.fetch_sub(1, atomic::Ordering::SeqCst); + if oldval == 1 { + drop(Box::from_raw(dev)); + std::ptr::null_mut() + } else { + dev + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_udev(dev: *const UdevDevice) -> *mut Udev { + unsafe { (*dev).udev } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_devpath(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { udev_device_get_property_value(dev, c"DEVPATH".as_ptr()) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_subsystem(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { udev_device_get_property_value(dev, c"SUBSYSTEM".as_ptr()) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_devtype(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { udev_device_get_property_value(dev, c"DEVTYPE".as_ptr()) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_syspath(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { + (*dev) + .syspath + .as_ref() + .map(|x| x.as_ptr()) + .unwrap_or_default() + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_sysname(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { + if dev.is_null() { + return std::ptr::null(); + } + let Some(syspath) = (*dev).syspath.as_ref() else { + return std::ptr::null(); + }; + let split_pos = syspath.as_bytes().iter().rposition(|x| *x == b'/'); + let offset = split_pos.map(|x| x + 1).unwrap_or_default(); + syspath.as_ptr().add(offset) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_sysnum(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { + let mut sysname = udev_device_get_sysname(dev); + let mut found: *const libc::c_char = std::ptr::null(); + if sysname.is_null() { + return std::ptr::null(); + } + while *sysname != 0 { + if found.is_null() && (*sysname as char).is_ascii_digit() { + found = sysname; + } + if !found.is_null() && (*sysname as char).is_ascii_alphabetic() { + found = std::ptr::null(); + } + sysname = sysname.add(1); + } + found + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_devnode(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { udev_device_get_property_value(dev, c"DEVNODE".as_ptr()) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_is_initialized(_: *const UdevDevice) -> libc::c_int { + 1 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_devlinks_list_entry( + dev: *const UdevDevice, +) -> *const UdevListEntry { + unsafe { &raw const (*dev).devlinks } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_properties_list_entry( + dev: *const UdevDevice, +) -> *const UdevListEntry { + unsafe { &raw const (*dev).properties } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_tags_list_entry( + dev: *const UdevDevice, +) -> *const UdevListEntry { + unsafe { &raw const (*dev).tags } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_current_tags_list_entry( + dev: *const UdevDevice, +) -> *const UdevListEntry { + unsafe { &raw const (*dev).tags } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_sysattr_list_entry( + dev: *const UdevDevice, +) -> *const UdevListEntry { + unsafe { &raw const (*dev).sysattr } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_property_value( + dev: *const UdevDevice, + key: *const libc::c_char, +) -> *const libc::c_char { + unsafe { udev_list_entry_get_value(udev_list_entry_get_by_name(&(*dev).properties, key)) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_driver(dev: *mut UdevDevice) -> *const libc::c_char { + unsafe { udev_device_get_sysattr_value(dev, c"driver".as_ptr()) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_devnum(dev: *const UdevDevice) -> libc::dev_t { + unsafe { + let devnode = udev_device_get_devnode(dev); + if devnode.is_null() { + return 0; + } + let mut statbuf = std::mem::zeroed(); + let status = libc::lstat(devnode, &mut statbuf); + if status != 0 { + return 0; + } + statbuf.st_rdev + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_action(dev: *const UdevDevice) -> *const libc::c_char { + unsafe { + (*dev) + .action + .as_ref() + .map(|x| x.as_ptr()) + .unwrap_or_default() + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_seqnum(dev: *const UdevDevice) -> libc::c_ulonglong { + unsafe { (*dev).seqnum } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_usec_since_initialized( + _: *const UdevDevice, +) -> libc::c_ulonglong { + 0 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_get_sysattr_value( + dev: *mut UdevDevice, + sysattr: *const libc::c_char, +) -> *const libc::c_char { + unsafe { + let sysattr = CStr::from_ptr(sysattr); + let Ok(sysattr_str) = sysattr.to_str() else { + return std::ptr::null(); + }; + if let Some(val) = (*dev).sysattr_cache.get(sysattr) { + return val.as_ptr(); + } + let Some(syspath) = (*dev).syspath.as_ref() else { + return std::ptr::null(); + }; + let Ok(mut path) = String::from_utf8(syspath.as_bytes().to_vec()) else { + return std::ptr::null(); + }; + path.push('/'); + path.push_str(sysattr_str); + let Ok(data) = std::fs::read_to_string(path) else { + return std::ptr::null(); + }; + let Ok(data) = CString::from_str(&data) else { + return std::ptr::null(); + }; + let ptr = data.as_ptr(); + (*dev).sysattr_cache.insert(CString::from(sysattr), data); + ptr + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_set_sysattr_value( + dev: *mut UdevDevice, + sysattr: *const libc::c_char, + value: *const libc::c_char, +) -> libc::c_int { + unsafe { + let dev = &mut *dev; + let sysattr = match CStr::from_ptr(sysattr).to_str() { + Ok(s) => s, + Err(_) => return -libc::EINVAL, + }; + let value = match CStr::from_ptr(value).to_str() { + Ok(s) => s, + Err(_) => return -libc::EINVAL, + }; + let syspath = match dev.syspath.as_ref() { + Some(p) => p, + None => return -libc::ENOENT, + }; + let path = match syspath.to_str() { + Ok(p) => format!("{}/{}", p, sysattr), + Err(_) => return -libc::ENOENT, + }; + match std::fs::write(&path, value) { + Ok(_) => { + if let Ok(cstr) = CString::new(value) { + let key = match CString::new(sysattr) { + Ok(k) => k, + Err(_) => return 0, + }; + dev.sysattr_cache.insert(key, cstr); + } + 0 + } + Err(e) => match e.raw_os_error() { + Some(code) => -code, + None => -libc::EIO, + }, + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_has_tag( + dev: *const UdevDevice, + tag: *const libc::c_char, +) -> libc::c_int { + unsafe { + (!udev_list_entry_get_by_name(udev_device_get_tags_list_entry(dev), tag).is_null()) as _ + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_device_has_current_tag( + dev: *const UdevDevice, + tag: *const libc::c_char, +) -> libc::c_int { + unsafe { + (!udev_list_entry_get_by_name(udev_device_get_tags_list_entry(dev), tag).is_null()) as _ + } +} + +#[derive(Debug)] +pub struct UdevMonitor { + udev: *mut Udev, + refcount: AtomicUsize, +} +impl Drop for UdevMonitor { + fn drop(&mut self) { + unsafe { + udev_unref(self.udev); + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_ref(mon: *mut UdevMonitor) -> *mut UdevMonitor { + unsafe { + (*mon).refcount.fetch_add(1, atomic::Ordering::SeqCst); + } + mon +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_unref(mon: *mut UdevMonitor) -> *mut UdevMonitor { + unsafe { + let oldval = (*mon).refcount.fetch_sub(1, atomic::Ordering::SeqCst); + if oldval == 1 { + drop(Box::from_raw(mon)); + std::ptr::null_mut() + } else { + mon + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_get_udev(mon: *const UdevMonitor) -> *mut Udev { + unsafe { (*mon).udev } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_new_from_netlink( + udev: *mut Udev, + name: *const libc::c_char, +) -> *mut UdevMonitor { + unsafe { + let name = CStr::from_ptr(name); + if name != c"udev" { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(UdevMonitor { + udev, + refcount: AtomicUsize::new(1), + })) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_enable_receiving(_: *const UdevMonitor) -> libc::c_int { + -libc::EPERM +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_set_receive_buffer_size( + _: *const UdevMonitor, + _: libc::c_int, +) -> libc::c_int { + 0 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_get_fd(_: *const UdevMonitor) -> libc::c_int { + -1 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_receive_device(_: *const UdevMonitor) -> *mut UdevDevice { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_filter_add_match_subsystem_devtype( + _mon: *mut UdevMonitor, + _subsystem: *const libc::c_char, + _devtype: *const libc::c_char, +) -> libc::c_int { + -libc::EPERM +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_filter_add_match_tag( + _mon: *mut UdevMonitor, + _tag: *const libc::c_char, +) -> libc::c_int { + -libc::EPERM +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_filter_update(_mon: *mut UdevMonitor) -> libc::c_int { + -libc::EPERM +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_monitor_filter_remove(_mon: *mut UdevMonitor) -> libc::c_int { + -libc::EPERM +} + +#[derive(Debug)] +pub struct UdevEnumerate { + udev: *mut Udev, + refcount: AtomicUsize, + filters: Vec, + result: Option, +} +impl Drop for UdevEnumerate { + fn drop(&mut self) { + unsafe { + udev_unref(self.udev); + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_ref(p: *mut UdevEnumerate) -> *mut UdevEnumerate { + unsafe { + (*p).refcount.fetch_add(1, atomic::Ordering::SeqCst); + } + p +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_unref(p: *mut UdevEnumerate) -> *mut UdevEnumerate { + unsafe { + let oldval = (*p).refcount.fetch_sub(1, atomic::Ordering::SeqCst); + if oldval == 1 { + drop(Box::from_raw(p)); + std::ptr::null_mut() + } else { + p + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_get_udev(p: *mut UdevEnumerate) -> *mut Udev { + unsafe { (*p).udev } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_new(udev: *mut Udev) -> *mut UdevEnumerate { + Box::into_raw(Box::new(UdevEnumerate { + udev, + refcount: AtomicUsize::new(1), + filters: Vec::with_capacity(16), + result: None, + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_subsystem( + udev_enumerate: *mut UdevEnumerate, + subsystem: *const libc::c_char, +) -> libc::c_int { + unsafe { udev_enumerate_match(udev_enumerate, "SUBSYSTEM", subsystem, false) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_nomatch_subsystem( + udev_enumerate: *mut UdevEnumerate, + subsystem: *const libc::c_char, +) -> libc::c_int { + unsafe { udev_enumerate_match(udev_enumerate, "SUBSYSTEM", subsystem, true) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_sysattr( + udev_enumerate: *mut UdevEnumerate, + sysattr: *const libc::c_char, + value: *const libc::c_char, +) -> libc::c_int { + unsafe { + let Ok(sysattr) = CStr::from_ptr(sysattr).to_str() else { + return -libc::EINVAL; + }; + udev_enumerate_match(udev_enumerate, sysattr, value, false) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_nomatch_sysattr( + udev_enumerate: *mut UdevEnumerate, + sysattr: *const libc::c_char, + value: *const libc::c_char, +) -> libc::c_int { + unsafe { + let Ok(sysattr) = CStr::from_ptr(sysattr).to_str() else { + return -libc::EINVAL; + }; + udev_enumerate_match(udev_enumerate, sysattr, value, true) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_property( + udev_enumerate: *mut UdevEnumerate, + property: *const libc::c_char, + value: *const libc::c_char, +) -> libc::c_int { + unsafe { + let Ok(property) = CStr::from_ptr(property).to_str() else { + return -libc::EINVAL; + }; + udev_enumerate_match(udev_enumerate, property, value, false) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_sysname( + udev_enumerate: *mut UdevEnumerate, + sysname: *const libc::c_char, +) -> libc::c_int { + unsafe { udev_enumerate_match(udev_enumerate, "SYSNAME", sysname, false) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_tag( + udev_enumerate: *mut UdevEnumerate, + tag: *const libc::c_char, +) -> libc::c_int { + unsafe { udev_enumerate_match(udev_enumerate, "TAGS", tag, false) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_parent( + _: *mut UdevEnumerate, + _: *mut libc::c_void, +) -> libc::c_int { + -libc::EOPNOTSUPP +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_match_is_initialized( + _: *mut UdevEnumerate, +) -> libc::c_int { + 0 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_add_syspath( + udev_enumerate: *mut UdevEnumerate, + syspath: *const libc::c_char, +) -> libc::c_int { + unsafe { udev_enumerate_match(udev_enumerate, "SYSPATH", syspath, false) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_scan_devices( + udev_enumerate: *mut UdevEnumerate, +) -> libc::c_int { + unsafe { + let udev = (*udev_enumerate).udev; + let when = When::All((*udev_enumerate).filters.clone()); + let result = match (*udev).client.list_devices(when) { + Ok(x) => x, + Err(_) => return -libc::EPERM, + }; + (*udev_enumerate).result = Some(*vec_to_list(&result)); + 0 + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_scan_subsystems( + udev_enumerate: *mut UdevEnumerate, +) -> libc::c_int { + unsafe { udev_enumerate_scan_devices(udev_enumerate) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_enumerate_get_list_entry( + udev_enumerate: *mut UdevEnumerate, +) -> *mut UdevListEntry { + unsafe { + (*udev_enumerate) + .result + .as_mut() + .map(|x| &raw mut *x) + .unwrap_or_default() + } +} + +unsafe fn udev_enumerate_match( + this: *mut UdevEnumerate, + key: &str, + data: *const libc::c_char, + not: bool, +) -> libc::c_int { + unsafe { + let Ok(data) = CStr::from_ptr(data).to_str() else { + return -libc::EINVAL; + }; + let mut when = When::Match(Str::Direct(key.into()), Str::Direct(data.into())); + if not { + when = When::Not(Box::new(when)); + } + (*this).filters.push(when); + 0 + } +} + +#[derive(Debug)] +pub struct UdevQueue; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_ref(_p: *mut UdevQueue) -> *mut UdevQueue { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_unref(_p: *mut UdevQueue) -> *mut UdevQueue { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_get_udev(_udev_queue: *mut UdevQueue) -> *mut Udev { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_new(_udev: *mut Udev) -> *mut UdevQueue { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_get_udev_is_active(_udev_queue: *mut UdevQueue) -> libc::c_int { + -1 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_get_queue_is_empty(_udev_queue: *mut UdevQueue) -> libc::c_int { + -1 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_get_fd(_udev_queue: *mut UdevQueue) -> libc::c_int { + -1 +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_queue_flush(_udev_queue: *mut UdevQueue) -> libc::c_int { + -1 +} + +#[derive(Debug)] +pub struct UdevHwdb; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_hwdb_new(_udev: *mut Udev) -> *mut UdevHwdb { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_hwdb_ref(_p: *mut UdevHwdb) -> *mut UdevHwdb { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_hwdb_unref(_p: *mut UdevHwdb) -> *mut UdevHwdb { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_hwdb_get_properties_list_entry( + _hwdb: *mut UdevHwdb, + _modalias: *const libc::c_char, + _flags: libc::c_uint, +) -> *mut UdevListEntry { + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn udev_util_encode_string( + s: *const std::ffi::c_char, + str_enc: *mut std::ffi::c_char, + len: usize, +) -> std::ffi::c_int { + unsafe { + libc::memcpy(str_enc.cast(), s.cast(), len); + } + len as _ +} + +fn hashmap_to_list(hashmap: &FxHashMap) -> Box { + let mut head = None; + let mut tail = &mut head; + + for (key, value) in hashmap { + let key_cstr = CString::new(key.as_str()).unwrap(); + let value_cstr = CString::new(value.as_str()).unwrap(); + let entry = Box::new(UdevListEntry { + key: key_cstr, + value: value_cstr, + next: None, + }); + *tail = Some(entry); + tail = &mut tail.as_mut().unwrap().next; + } + + head.unwrap_or(Box::new(UdevListEntry { + key: CString::new("").unwrap(), + value: CString::new("").unwrap(), + next: None, + })) +} + +fn vec_to_list(vec: &Vec) -> Box { + let mut head = None; + let mut tail = &mut head; + + for s in vec { + let cstr = CString::new(s.as_str()).unwrap(); + let entry = Box::new(UdevListEntry { + key: cstr, + value: CString::new("").unwrap(), + next: None, + }); + *tail = Some(entry); + tail = &mut tail.as_mut().unwrap().next; + } + + head.unwrap_or_else(|| { + Box::new(UdevListEntry { + key: CString::new("").unwrap(), + value: CString::new("").unwrap(), + next: None, + }) + }) +} diff --git a/libs/ipc/Cargo.toml b/libs/ipc/Cargo.toml new file mode 100644 index 0000000..f9d19a8 --- /dev/null +++ b/libs/ipc/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ipc" +version = "0.1.0" +edition = "2024" + +[dependencies] +rule = { path = "../rule" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tokio = { version = "1", features = ["net", "io-util"] } diff --git a/libs/ipc/src/client.rs b/libs/ipc/src/client.rs new file mode 100644 index 0000000..71fc76b --- /dev/null +++ b/libs/ipc/src/client.rs @@ -0,0 +1,63 @@ +use crate::{ + SOCKET_NAME, + repr::{Error, Request, de_reply}, +}; +use rule::parser::When; +use serde::de::DeserializeOwned; +use std::{ + io::{Read, Write}, + os::{ + linux::net::SocketAddrExt, + unix::net::{SocketAddr, UnixStream}, + }, +}; + +#[derive(Debug)] +pub struct Client { + stream: UnixStream, + buf: Vec, +} +impl Client { + pub fn connect() -> std::io::Result { + let addr = SocketAddr::from_abstract_name(SOCKET_NAME)?; + Ok(Self { + stream: UnixStream::connect_addr(&addr)?, + buf: Vec::with_capacity(128), + }) + } + + pub fn send(&mut self, req: &Request) -> std::io::Result<()> { + self.buf.clear(); + serde_json::to_writer(&mut self.buf, req).unwrap(); + self.stream + .write_all(&(self.buf.len() as u16).to_le_bytes())?; + self.stream.write_all(&self.buf)?; + Ok(()) + } + + pub fn recv(&mut self) -> std::io::Result> { + let mut len = [0u8; size_of::()]; + self.stream.read_exact(&mut len)?; + let len = u16::from_le_bytes(len); + self.buf.resize(len as usize, 0); + self.stream.read_exact(&mut self.buf)?; + Ok(de_reply(&String::from_utf8_lossy(&self.buf))) + } + + pub fn invoke(&mut self, req: &Request) -> Result { + self.send(req).map_err(|_| Error::Delivery)?; + self.recv().map_err(|_| Error::Delivery).flatten() + } + + pub fn trigger(&mut self, set: String, action: String) -> Result<(), Error> { + self.invoke(&Request::Trigger(set, action)) + } + + pub fn reload_rules(&mut self) -> Result<(), Error> { + self.invoke(&Request::ReloadRules) + } + + pub fn list_devices(&mut self, when: When) -> Result, Error> { + self.invoke(&Request::ListDevices(when)) + } +} diff --git a/libs/ipc/src/lib.rs b/libs/ipc/src/lib.rs new file mode 100644 index 0000000..526069a --- /dev/null +++ b/libs/ipc/src/lib.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod repr; +pub mod server; + +pub const SOCKET_NAME: &str = "verified:org.semilabs.os/lxdeviced"; diff --git a/libs/ipc/src/repr.rs b/libs/ipc/src/repr.rs new file mode 100644 index 0000000..c92847e --- /dev/null +++ b/libs/ipc/src/repr.rs @@ -0,0 +1,64 @@ +use rule::parser::When; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "method", content = "params")] +pub enum Request { + #[serde(rename = "/org.semilabs.os/LxDeviced/Trigger@LXDEVICED_0.1.0")] + Trigger(String, String), + + #[serde(rename = "/org.semilabs.os/LxDeviced/ReloadRules@LXDEVICED_0.1.0")] + ReloadRules, + + #[serde(rename = "/org.semilabs.os/LxDeviced/ListDevices@LXDEVICED_0.1.0")] + ListDevices(When), +} + +pub fn ser_reply(result: Result) -> String { + const FALLBACK: &str = r#"{"error": "/AnyCall/BadReply@ANYCALL_LITE_1"}"#; + + let reply = match result { + Ok(x) => Reply::Ok(x), + Err(e) => Reply::Err(e), + }; + serde_json::to_string(&reply).unwrap_or_else(|_| FALLBACK.into()) +} + +pub fn de_reply(s: &str) -> Result { + let reply: Reply = serde_json::from_str(s).map_err(|_| Error::BadReply)?; + match reply { + Reply::Ok(x) => Ok(x), + Reply::Err(e) => Err(e), + } +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum Reply { + Err(Error), + Ok(T), +} + +#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)] +#[serde(tag = "error")] +pub enum Error { + #[serde(rename = "/AnyCall/BadRequest@ANYCALL_LITE_1")] + #[error("Invalid Arguments")] + BadRequest, + + #[serde(rename = "/AnyCall/BadReply@ANYCALL_LITE_1")] + #[error("Bad Reply")] + BadReply, + + #[serde(rename = "/AnyCall/NotImplemented@ANYCALL_LITE_1")] + #[error("Not Implemented")] + NotImplemented, + + #[serde(rename = "/AnyCall/PermissionDenied@ANYCALL_LITE_1")] + #[error("Permission Denied")] + PermissionDenied, + + #[serde(rename = "/AnyCall/DeliveryError@ANYCALL_LITE_1")] + #[error("Delivery Error")] + Delivery, +} diff --git a/libs/ipc/src/server.rs b/libs/ipc/src/server.rs new file mode 100644 index 0000000..e8d1eec --- /dev/null +++ b/libs/ipc/src/server.rs @@ -0,0 +1,61 @@ +use crate::{ + SOCKET_NAME, + repr::{Error, Request, ser_reply}, +}; +use serde::Serialize; +use std::os::{linux::net::SocketAddrExt, unix::net::SocketAddr as StdSocketAddr}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{UnixListener, UnixStream, unix::SocketAddr}, +}; + +#[derive(Debug)] +pub struct Listener(UnixListener); +impl Listener { + pub fn bind() -> std::io::Result { + let addr = SocketAddr::from(StdSocketAddr::from_abstract_name(SOCKET_NAME)?); + UnixListener::bind_addr(&addr).map(Self) + } + + pub async fn accept(&self) -> std::io::Result { + let stream = self.0.accept().await?.0; + let cred = stream.peer_cred()?; + Ok(Connection { + stream, + buf: Vec::with_capacity(128), + uid: cred.uid(), + gid: cred.gid(), + pid: cred.pid().unwrap_or_default() as _, + }) + } +} + +#[derive(Debug)] +pub struct Connection { + stream: UnixStream, + buf: Vec, + pub uid: u32, + pub gid: u32, + pub pid: u32, +} +impl Connection { + pub async fn recv(&mut self) -> std::io::Result { + loop { + let size = self.stream.read_u16_le().await?; + self.buf.resize(size as usize, 0); + self.stream.read_exact(&mut self.buf).await?; + let Ok(request) = serde_json::from_slice(&self.buf) else { + self.send::<()>(Err(Error::BadRequest)).await?; + continue; + }; + break Ok(request); + } + } + + pub async fn send(&mut self, reply: Result) -> std::io::Result<()> { + let reply = ser_reply(reply); + self.stream.write_u16_le(reply.len() as u16).await?; + self.stream.write_all(reply.as_bytes()).await?; + Ok(()) + } +} diff --git a/libs/kobject-uevent/Cargo.toml b/libs/kobject-uevent/Cargo.toml new file mode 100644 index 0000000..054dc2d --- /dev/null +++ b/libs/kobject-uevent/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "kobject-uevent" +version = "0.1.0" +edition = "2024" + +[dependencies] +netlink-sys = { version = "0.9", features = ["tokio_socket"] } +rustc-hash = "2" diff --git a/libs/kobject-uevent/src/lib.rs b/libs/kobject-uevent/src/lib.rs new file mode 100644 index 0000000..681ef82 --- /dev/null +++ b/libs/kobject-uevent/src/lib.rs @@ -0,0 +1,87 @@ +use netlink_sys::{ + AsyncSocket, AsyncSocketExt, SocketAddr, TokioSocket, protocols::NETLINK_KOBJECT_UEVENT, +}; +use rustc_hash::FxHashMap; + +pub const MULTICAST_GROUP_KERNEL: u32 = 1; + +pub struct KobjectUeventSocket(TokioSocket); +impl KobjectUeventSocket { + pub fn connect(group: u32) -> std::io::Result { + let mut socket = TokioSocket::new(NETLINK_KOBJECT_UEVENT)?; + let addr = SocketAddr::new(0, group); + socket.socket_mut().bind(&addr)?; + Ok(Self(socket)) + } + + pub async fn recv(&self) -> std::io::Result { + let (data, sender) = self.0.recv_from_full().await?; + if sender.port_number() != 0 { + return Err(std::io::ErrorKind::PermissionDenied.into()); + } + Ok(KobjectUevent::parse(data.as_slice())) + } +} + +#[derive(Debug, Clone)] +pub struct KobjectUevent(FxHashMap); +impl KobjectUevent { + pub fn inner(&self) -> &FxHashMap { + &self.0 + } + pub fn parse(s: &[u8]) -> Self { + let mut map = FxHashMap::default(); + for kvp in s.split(|x| *x == b'\0') { + let Some(sep) = kvp.iter().position(|x| *x == b'=') else { + continue; + }; + let (key, val) = kvp.split_at(sep); + let val = &val[1..]; + map.insert( + String::from_utf8_lossy_owned(key.into()), + String::from_utf8_lossy_owned(val.into()), + ); + } + Self(map) + } + + pub fn action(&self) -> Option<&str> { + self.0.get("ACTION").map(String::as_str) + } + + pub fn devpath(&self) -> Option<&str> { + self.0.get("DEVPATH").map(String::as_str) + } + + pub fn subsystem(&self) -> Option<&str> { + self.0.get("SUBSYSTEM").map(String::as_str) + } + + pub fn synth_uuid(&self) -> Option<&str> { + self.0.get("SYNTH_UUID").map(String::as_str) + } + + pub fn major(&self) -> Option { + let major = self.0.get("MAJOR")?; + major.parse().ok() + } + + pub fn minor(&self) -> Option { + let minor = self.0.get("MINOR")?; + minor.parse().ok() + } + + pub fn devname(&self) -> Option<&str> { + self.0.get("DEVNAME").map(String::as_str) + } + + pub fn devmode(&self) -> Option { + let devmode = self.0.get("DEVMODE")?; + let devmode = devmode.strip_prefix("0")?; + u16::from_str_radix(devmode, 8).ok() + } + + pub fn seqnum(&self) -> Option<&str> { + self.0.get("SEQNUM").map(String::as_str) + } +} diff --git a/libs/rule/Cargo.toml b/libs/rule/Cargo.toml new file mode 100644 index 0000000..3ca360d --- /dev/null +++ b/libs/rule/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rule" +version = "0.1.0" +edition = "2024" + +[dependencies] +libc = "0.2" +regex = "1" +ron = "0.12" +rustc-hash = "2" +serde = { version = "1", features = ["derive"] } +thiserror = "2" +tokio = { version = "1", features = ["process", "time"] } +tracing = "0.1" diff --git a/libs/rule/src/engine.rs b/libs/rule/src/engine.rs new file mode 100644 index 0000000..86dd21c --- /dev/null +++ b/libs/rule/src/engine.rs @@ -0,0 +1,359 @@ +use crate::{ + Error, + parser::{Action, OnError, Rule, RuleFile, Str, When}, +}; +use regex::Regex; +use rustc_hash::FxHashMap; +use std::{ + path::Path, + process::{ExitStatus, Stdio}, + sync::{Arc, RwLock}, + time::Duration, +}; +use tokio::{io::AsyncReadExt, process::Child}; + +#[derive(Debug, Clone)] +pub struct RuleSet(Arc>); +impl RuleSet { + pub fn empty() -> Self { + Self(Arc::default()) + } + + pub fn open_dirs(dirs: impl Iterator>) -> Self { + let mut rule_files = Vec::with_capacity(32); + for dir in dirs { + let Ok(read_dir) = std::fs::read_dir(dir.as_ref()) else { + tracing::debug!( + "Did not read directory \"{}\" for rule files.", + dir.as_ref().display() + ); + continue; + }; + for file in read_dir { + let Ok(file) = file else { + tracing::warn!( + "Error while reading directory \"{}\" for rule files.", + dir.as_ref().display() + ); + break; + }; + if file.file_name().as_encoded_bytes().starts_with(b".") { + continue; + } + let rule_file = match RuleFile::parse(file.path()) { + Ok(x) => x, + Err(e) => { + tracing::warn!( + "Failed to parse rule file \"{}\": {}", + file.path().display(), + e, + ); + continue; + } + }; + tracing::debug!("Activating rule file \"{}\"", file.path().display()); + rule_files.push(rule_file); + } + } + Self(Arc::new(rule_files)) + } +} + +#[derive(Debug)] +pub struct Engine { + properties: RwLock>, + command_policy: CommandPolicy, +} +impl Engine { + pub fn new() -> Self { + Self { + properties: Default::default(), + command_policy: CommandPolicy::default(), + } + } + + pub fn get_property(&self, key: &str) -> Option { + self.properties.read().unwrap().get(key).cloned() + } + + pub fn set_property(&self, key: String, val: String) { + self.properties.write().unwrap().insert(key, val); + } + + pub fn remove_property(&self, key: &str) { + self.properties.write().unwrap().remove(key); + } + + pub fn get_list(&self, key: &str) -> Vec { + self.get_property(key) + .unwrap_or_default() + .split('\0') + .filter(|x| !x.is_empty()) + .map(Into::into) + .collect() + } + + pub fn list_add(&self, key: String, val: &str) { + let orig = self.get_property(&key).unwrap_or_default(); + self.set_property(key, format!("{orig}{val}\0")); + } + + pub async fn exec(&self, rule_set: &RuleSet) { + for rule_file in rule_set.0.iter() { + for rule in rule_file.rules.iter() { + if let Err(err) = self.exec_rule(rule).await { + if !matches!(rule.on_error, OnError::Continue) { + tracing::warn!("Rule \"{}\" failed: {err}", rule_file.path.display()); + } + match rule.on_error { + OnError::Continue => {} + OnError::Log => {} + OnError::StopFile => break, + OnError::StopAll => return, + } + } + } + } + } + + async fn exec_rule(&self, rule: &Rule) -> Result<(), Error> { + if self.exec_condition(&rule.when)? { + for action in rule.actions.iter() { + self.exec_action(&action).await?; + } + } + Ok(()) + } + + pub fn exec_condition(&self, when: &When) -> Result { + match when { + When::All(children) => { + let mut result = true; + for child in children { + result &= self.exec_condition(child)?; + } + Ok(result) + } + When::Any(children) => { + let mut result = false; + for child in children { + result |= self.exec_condition(child)?; + } + Ok(result) + } + When::Not(child) => self.exec_condition(child).map(|x| !x), + When::True => Ok(true), + When::False => Ok(false), + When::Match(key, regex) => { + let key = self.bake_str(key)?; + let regex = self.bake_str(regex)?; + let regex = format!("^{regex}$"); + let regex = Regex::new(®ex).map_err(Error::Regex)?; + let Some(val) = self.get_property(&key) else { + return Ok(false); + }; + Ok(regex.is_match(&val)) + } + When::PartMatch(key, regex) => { + let key = self.bake_str(key)?; + let regex = self.bake_str(regex)?; + let regex = Regex::new(®ex).map_err(Error::Regex)?; + let Some(val) = self.get_property(&key) else { + return Ok(false); + }; + Ok(regex.is_match(&val)) + } + } + } + + async fn exec_action(&self, action: &Action) -> Result<(), Error> { + match action { + Action::Set(key, val) => { + let key = self.bake_str(key)?; + let val = self.bake_str(val)?; + self.set_property(key, val); + } + Action::Add(key, val) => { + let key = self.bake_str(key)?; + let val = self.bake_str(val)?; + self.list_add(key, &val); + } + Action::Unset(key) => { + self.remove_property(&self.bake_str(key)?); + } + Action::Command(cmd) => { + let cmd = self.bake_str(cmd)?; + self.command_policy.run(&cmd).await?; + } + Action::UseProvider(key, cmd) => { + let key = self.bake_str(key)?; + let cmd = self.bake_str(cmd)?; + let val = self.command_policy.run_provider(&cmd).await?; + self.set_property(key, val); + } + } + Ok(()) + } + + fn bake_str(&self, s: &Str) -> Result { + match s { + Str::Direct(x) => Ok(x.into()), + Str::Format(template) => { + let mut result = String::with_capacity(template.len()); + let mut current_property: Option = None; + let mut right_bracket = false; + for ch in template.chars() { + if right_bracket { + if ch == '}' { + right_bracket = false; + result.push('}'); + } else { + return Err(Error::InvalidStringTemplate); + } + } else if let Some(property) = &mut current_property { + if property.is_empty() && ch == '{' { + result.push('{'); + current_property = None; + } else if ch == '}' { + let value = self + .get_property(property) + .ok_or_else(|| Error::PropertyNotFound(property.clone()))?; + result.push_str(&value); + current_property = None; + } else { + property.push(ch); + } + } else if ch == '{' { + current_property = Some(String::with_capacity(16)); + } else if ch == '}' { + right_bracket = true; + } else { + result.push(ch); + } + } + if current_property.is_some() || right_bracket { + return Err(Error::InvalidStringTemplate); + } + Ok(result) + } + } + } +} +impl Default for Engine { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub struct CommandPolicy { + prefix: Vec, + pub timeout: Duration, + pub timeout_behavior: CommandTimeoutBehavior, + pub info_limit: usize, +} +impl CommandPolicy { + pub fn new() -> Self { + Self { + prefix: vec![String::from("/bin/sh"), String::from("-c")], + timeout: Duration::from_secs(30), + timeout_behavior: CommandTimeoutBehavior::Ignore, + info_limit: 1024, + } + } + + pub async fn run(&self, cmd: &str) -> Result<(), Error> { + let mut child = self + ._command(cmd) + .spawn() + .map_err(|e| Error::Command(cmd.into(), e.to_string()))?; + let timeout_wait = tokio::time::timeout(self.timeout, child.wait()).await; + match timeout_wait { + Ok(Ok(wait)) => self._wait_success(cmd, wait), + Ok(Err(_)) => Err(Error::Command(cmd.into(), "wait() failure".into())), + Err(_) => self._timeout(cmd, child), + } + } + + pub async fn run_provider(&self, cmd: &str) -> Result { + let mut child = self + ._command(cmd) + .stdout(Stdio::piped()) + .spawn() + .map_err(|e| Error::Command(cmd.into(), e.to_string()))?; + let mut stdout = String::with_capacity(128); + let timeout_wait = tokio::time::timeout(self.timeout, async { + child + .stdout + .take() + .unwrap() + .take(self.info_limit as _) + .read_to_string(&mut stdout) + .await?; + child.wait().await + }) + .await; + match timeout_wait { + Ok(Ok(wait)) => { + self._wait_success(cmd, wait)?; + Ok(stdout.trim().into()) + } + Ok(Err(_)) => Err(Error::Command(cmd.into(), "wait() failure".into())), + Err(_) => { + self._timeout(cmd, child)?; + unreachable!(); + } + } + } + + fn _wait_success(&self, cmd: &str, wait: ExitStatus) -> Result<(), Error> { + if wait.success() { + Ok(()) + } else { + Err(Error::Command( + cmd.into(), + format!( + "process exited with code {}", + wait.code().unwrap_or(i32::MIN), + ), + )) + } + } + + fn _command(&self, cmd: &str) -> tokio::process::Command { + let mut command = tokio::process::Command::new(&self.prefix[0]); + command.args(&self.prefix[1..]).arg(cmd); + command + } + + fn _timeout(&self, cmd: &str, mut child: Child) -> Result<(), Error> { + match self.timeout_behavior { + CommandTimeoutBehavior::Ignore => {} + CommandTimeoutBehavior::SoftKill => unsafe { + if let Some(pid) = child.id() { + libc::kill(pid as _, libc::SIGTERM); + } + }, + CommandTimeoutBehavior::HardKill => _ = child.start_kill(), + }; + Err(Error::Command(cmd.into(), "Timed out".into())) + } +} +impl Default for CommandPolicy { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub enum CommandTimeoutBehavior { + #[default] + Ignore, + SoftKill, + HardKill, +} +impl CommandTimeoutBehavior { + pub fn new() -> Self { + Self::default() + } +} diff --git a/libs/rule/src/error.rs b/libs/rule/src/error.rs new file mode 100644 index 0000000..1c6c737 --- /dev/null +++ b/libs/rule/src/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Read(std::io::Error), + + #[error("{0}")] + Parse(ron::de::SpannedError), + + #[error("Property \"{0}\" not found")] + PropertyNotFound(String), + + #[error("Invalid format string template")] + InvalidStringTemplate, + + #[error("Regex error: {0}")] + Regex(regex::Error), + + #[error("Failed to execute command \"{0}\": {1}")] + Command(String, String), +} diff --git a/libs/rule/src/lib.rs b/libs/rule/src/lib.rs new file mode 100644 index 0000000..d588dbe --- /dev/null +++ b/libs/rule/src/lib.rs @@ -0,0 +1,5 @@ +pub mod engine; +pub mod error; +pub mod parser; + +pub use error::Error; diff --git a/libs/rule/src/parser.rs b/libs/rule/src/parser.rs new file mode 100644 index 0000000..a3a8a8f --- /dev/null +++ b/libs/rule/src/parser.rs @@ -0,0 +1,124 @@ +use crate::Error; +use serde::{Deserialize, Serialize, ser::SerializeTupleVariant}; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct RuleFile { + pub path: PathBuf, + pub rules: Vec, +} +impl RuleFile { + pub fn parse(path: impl Into) -> Result { + let path = path.into(); + let s = std::fs::read_to_string(&path).map_err(Error::Read)?; + let rules = ron::from_str::>(&s).map_err(Error::Parse)?; + Ok(Self { path, rules }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Rule { + pub when: When, + + #[serde(alias = "do")] + pub actions: Vec, + + #[serde(default)] + pub on_error: OnError, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum When { + All(Vec), + Any(Vec), + Not(Box), + True, + False, + Match(Str, Str), + PartMatch(Str, Str), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Action { + Command(Str), + Add(Str, Str), + Set(Str, Str), + Unset(Str), + UseProvider(Str, Str), +} + +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub enum OnError { + /// Continue the workflow, without logging messages. + Continue, + + /// Continue the workflow, but outputs a warning log. + #[default] + Log, + + /// Stop the file. + StopFile, + + /// Stop the workflow. + StopAll, +} + +#[derive(Debug, Clone)] +pub enum Str { + Direct(String), + Format(String), +} +impl Serialize for Str { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Direct(s) => s.serialize(serializer), + Self::Format(s) => { + let mut ser = serializer.serialize_tuple_variant("Str", 1, "f", 1)?; + ser.serialize_field(s)?; + ser.end() + } + } + } +} +impl<'de> Deserialize<'de> for Str { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct StrVisitor; + impl<'de> serde::de::Visitor<'de> for StrVisitor { + type Value = Str; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string or array with a single string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(Str::Direct(v.to_string())) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let value = seq + .next_element::()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + + if seq.next_element::()?.is_some() { + return Err(serde::de::Error::invalid_length(2, &self)); + } + + Ok(Str::Format(value)) + } + } + + deserializer.deserialize_any(StrVisitor) + } +} diff --git a/misc/airup/lxdeviced.airs b/misc/airup/lxdeviced.airs new file mode 100644 index 0000000..f519ca9 --- /dev/null +++ b/misc/airup/lxdeviced.airs @@ -0,0 +1,19 @@ +[service] +display-name = "Linux Device Daemon" +description = "Userspace daemon for dynamically managing Linux device files and their properties." + +[exec] +start = "lxdeviced" + +[env] +working_dir = "/" +clear_vars = true + +[retry] +max-attempts = -1 + +[env.vars] +PATH = "/usr/bin" +LXDEVICED_HAS_DEVTMPFS = "1" +LXDEVICED_DEV_ROOT = "/dev" +LXDEVICED_SYSFS_ROOT = "/sys" diff --git a/misc/rules/0010-basic-pseudo-dev.ron b/misc/rules/0010-basic-pseudo-dev.ron new file mode 100644 index 0000000..78ecdf1 --- /dev/null +++ b/misc/rules/0010-basic-pseudo-dev.ron @@ -0,0 +1,66 @@ +[ + ( + when: Match("DEVPATH", "/devices/virtual/mem/null"), + do: [ + Set("MODE", "0o666"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/zero"), + do: [ + Set("MODE", "0o666"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/urandom"), + do: [ + Set("MODE", "0o666"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/random"), + do: [ + Set("MODE", "0o666"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/full"), + do: [ + Set("MODE", "0o666"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/mem"), + do: [ + Set("MODE", "0o600"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/port"), + do: [ + Set("MODE", "0o600"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), + ( + when: Match("DEVPATH", "/devices/virtual/mem/kmsg"), + do: [ + Set("MODE", "0o600"), + Set("OWNER", "0"), + Set("GROUP", "0"), + ], + ), +] diff --git a/misc/rules/0015-kmod.ron b/misc/rules/0015-kmod.ron new file mode 100644 index 0000000..9d3869e --- /dev/null +++ b/misc/rules/0015-kmod.ron @@ -0,0 +1,8 @@ +[ + ( + when: Match("MODALIAS", ".*"), + do: [ + Command(f("modprobe {MODALIAS}")), + ], + ), +] diff --git a/misc/rules/0020-tty.ron b/misc/rules/0020-tty.ron new file mode 100644 index 0000000..d26093e --- /dev/null +++ b/misc/rules/0020-tty.ron @@ -0,0 +1,17 @@ +[ + ( + when: Match("SUBSYSTEM", "tty"), + do: [ + Set("MODE", "0o600"), + ] + ), + ( + when: All([ + Match("SUBSYSTEM", "tty"), + Match("DEVNAME", "tty"), + ]), + do: [ + Set("MODE", "0o666"), + ] + ), +] diff --git a/misc/rules/0050-blockdev.ron b/misc/rules/0050-blockdev.ron new file mode 100644 index 0000000..6ccddf0 --- /dev/null +++ b/misc/rules/0050-blockdev.ron @@ -0,0 +1,54 @@ +[ + // Register information providers that knows block devices + ( + when: All([ + Match("ACTION", "add"), + Match("SUBSYSTEM", "block"), + Match("DEVNODE", ".*"), + ]), + do: [ + UseProvider("FSUUID", f("blkid -o value -s UUID {DEVNODE}")), + ], + on_error: Continue, + ), + ( + when: Match("FSUUID", ""), + do: [Unset("FSUUID")], + ), + + // Create link aliases for block devices + ( + when: All([ + Match("ACTION", "add"), + Match("SUBSYSTEM", "block"), + Match("PARTUUID", ".+"), + ]), + do: [Add("SYMLINKS", f("{DEVROOT}/disk/by-partuuid/{PARTUUID}"))], + ), + ( + when: All([ + Match("ACTION", "add"), + Match("SUBSYSTEM", "block"), + Match("DISKSEQ", ".+"), + Match("DEVTYPE", "disk"), + ]), + do: [Add("SYMLINKS", f("{DEVROOT}/disk/by-diskseq/{DISKSEQ}"))], + ), + ( + when: All([ + Match("ACTION", "add"), + Match("SUBSYSTEM", "block"), + Match("FSUUID", ".+"), + ]), + do: [Add("SYMLINKS", f("{DEVROOT}/disk/by-uuid/{FSUUID}"))], + ), + ( + when: All([ + Match("ACTION", "add"), + Match("SUBSYSTEM", "block"), + Match("MAJOR", ".+"), + Match("MINOR", ".+"), + ]), + do: [Add("SYMLINKS", f("{DEVROOT}/block/{MAJOR}:{MINOR}"))], + ), +]