use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum RuleAction {
Allow,
Disallow,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub struct OsCondition {
pub name: Option<String>,
pub arch: Option<String>,
pub version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct FeatureSet {
pub is_demo_user: Option<bool>,
pub has_custom_resolution: Option<bool>,
pub has_quick_plays_support: Option<bool>,
pub is_quick_play_singleplayer: Option<bool>,
pub is_quick_play_multiplayer: Option<bool>,
pub is_quick_play_realms: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Rule {
pub action: RuleAction,
pub os: Option<OsCondition>,
pub features: Option<FeatureSet>,
}
pub struct RuleContext<'a> {
pub os_name: &'a str,
pub os_version: &'a str,
pub arch: &'a str,
pub features: &'a FeatureSet,
}
pub fn evaluate(rules: &[Rule], ctx: &RuleContext) -> bool {
if rules.is_empty() {
return true;
}
let mut allowed = false;
for rule in rules {
if rule_matches(rule, ctx) {
allowed = matches!(rule.action, RuleAction::Allow);
}
}
allowed
}
fn rule_matches(rule: &Rule, ctx: &RuleContext) -> bool {
if let Some(os) = &rule.os {
if let Some(name) = &os.name
&& name != ctx.os_name
{
return false;
}
if let Some(arch) = &os.arch
&& arch != ctx.arch
{
return false;
}
if let Some(pattern) = &os.version
&& !os_version_matches(pattern, ctx.os_version)
{
return false;
}
}
if let Some(required) = &rule.features
&& !features_match(required, ctx.features)
{
return false;
}
true
}
fn os_version_matches(pattern: &str, host_version: &str) -> bool {
if host_version.is_empty() {
return false;
}
let needle = pattern
.trim_start_matches('^')
.trim_end_matches('$')
.trim_end_matches('.')
.trim_end_matches('\\');
host_version.contains(needle)
}
fn features_match(required: &FeatureSet, current: &FeatureSet) -> bool {
let pairs = [
(required.is_demo_user, current.is_demo_user),
(
required.has_custom_resolution,
current.has_custom_resolution,
),
(
required.has_quick_plays_support,
current.has_quick_plays_support,
),
(
required.is_quick_play_singleplayer,
current.is_quick_play_singleplayer,
),
(
required.is_quick_play_multiplayer,
current.is_quick_play_multiplayer,
),
(required.is_quick_play_realms, current.is_quick_play_realms),
];
pairs.iter().all(|(req, cur)| match req {
Some(want) => cur.unwrap_or(false) == *want,
None => true,
})
}
#[cfg(test)]
#[path = "tests/rules.rs"]
mod tests;