Initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-09-01 13:01:13 +00:00
commit 5163bdc367
34 changed files with 2625 additions and 0 deletions
+14
View File
@@ -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"
+359
View File
@@ -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<Vec<RuleFile>>);
impl RuleSet {
pub fn empty() -> Self {
Self(Arc::default())
}
pub fn open_dirs(dirs: impl Iterator<Item = impl AsRef<Path>>) -> 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<FxHashMap<String, String>>,
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<String> {
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<String> {
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<bool, Error> {
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(&regex).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(&regex).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<String, Error> {
match s {
Str::Direct(x) => Ok(x.into()),
Str::Format(template) => {
let mut result = String::with_capacity(template.len());
let mut current_property: Option<String> = 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<String>,
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<String, Error> {
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()
}
}
+20
View File
@@ -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),
}
+5
View File
@@ -0,0 +1,5 @@
pub mod engine;
pub mod error;
pub mod parser;
pub use error::Error;
+124
View File
@@ -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<Rule>,
}
impl RuleFile {
pub fn parse(path: impl Into<PathBuf>) -> Result<Self, Error> {
let path = path.into();
let s = std::fs::read_to_string(&path).map_err(Error::Read)?;
let rules = ron::from_str::<Vec<Rule>>(&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<Action>,
#[serde(default)]
pub on_error: OnError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum When {
All(Vec<When>),
Any(Vec<When>),
Not(Box<When>),
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Str::Direct(v.to_string()))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let value = seq
.next_element::<String>()?
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
if seq.next_element::<String>()?.is_some() {
return Err(serde::de::Error::invalid_length(2, &self));
}
Ok(Str::Format(value))
}
}
deserializer.deserialize_any(StrVisitor)
}
}