Skip to main content

opys_core/
shorthand.rs

1//! String-form shorthand for rules: `"allow.os.linux@10\\."` ↔ `Rule`.
2//!
3//! Mirrors `core/lib/shorthand.ts`.
4
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use thiserror::Error;
8use opys_mojang_rules::{OsArch, OsConstraint, OsName, Rule, RuleAction, Ruleset};
9
10#[derive(Debug, Error)]
11pub enum ShorthandError {
12    #[error("Unknown action '{0}'")]
13    UnknownAction(String),
14    #[error("missing OS name")]
15    MissingOsName,
16    #[error("missing feature name")]
17    MissingFeature,
18    #[error("missing arch")]
19    MissingArch,
20    #[error("unknown rule type '{0}'")]
21    UnknownRuleType(String),
22    #[error("invalid os name '{0}'")]
23    InvalidOsName(String),
24    #[error("invalid arch '{0}'")]
25    InvalidArch(String),
26}
27
28/// Wire form of a single rule entry: either a shorthand string or an expanded
29/// rule object.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(untagged)]
32pub enum RawSingle {
33    Short(String),
34    Expanded(Rule),
35}
36
37/// Wire form of a ruleset: a single entry or an array.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(untagged)]
40pub enum RawRuleset {
41    One(RawSingle),
42    Many(Vec<RawSingle>),
43}
44
45fn parse_os_name(s: &str) -> Result<OsName, ShorthandError> {
46    match s {
47        "linux" => Ok(OsName::Linux),
48        "windows" => Ok(OsName::Windows),
49        "osx" => Ok(OsName::Osx),
50        _ => Err(ShorthandError::InvalidOsName(s.to_owned())),
51    }
52}
53
54fn parse_arch(s: &str) -> Result<OsArch, ShorthandError> {
55    match s {
56        "x86" => Ok(OsArch::X86),
57        "x86_64" => Ok(OsArch::X86_64),
58        "arm" => Ok(OsArch::Arm),
59        "aarch64" => Ok(OsArch::Aarch64),
60        "any" => Ok(OsArch::Any),
61        _ => Err(ShorthandError::InvalidArch(s.to_owned())),
62    }
63}
64
65pub fn parse_short_rule(raw: RawSingle) -> Result<Rule, ShorthandError> {
66    let s = match raw {
67        RawSingle::Expanded(r) => return Ok(r),
68        RawSingle::Short(s) => s,
69    };
70
71    let mut parts = s.splitn(3, '.');
72    let action_str = parts.next().unwrap_or("");
73    let action = match action_str {
74        "allow" => RuleAction::Allow,
75        "disallow" => RuleAction::Disallow,
76        other => return Err(ShorthandError::UnknownAction(other.to_owned())),
77    };
78    let type_part = parts.next();
79    let rest = parts.next().unwrap_or("");
80
81    let Some(typ) = type_part else {
82        return Ok(Rule::Plain { action });
83    };
84
85    match typ {
86        "os" => {
87            if rest.is_empty() {
88                return Err(ShorthandError::MissingOsName);
89            }
90            let (name_part, version) = match rest.find('@') {
91                Some(i) => (&rest[..i], Some(rest[i + 1..].to_owned())),
92                None => (rest, None),
93            };
94            let name = parse_os_name(name_part)?;
95            Ok(Rule::Os {
96                action,
97                os: OsConstraint {
98                    name: Some(name),
99                    version,
100                    arch: None,
101                },
102            })
103        }
104        "features" => {
105            if rest.is_empty() {
106                return Err(ShorthandError::MissingFeature);
107            }
108            let mut m = BTreeMap::new();
109            m.insert(rest.to_owned(), true);
110            Ok(Rule::Features {
111                action,
112                features: m,
113            })
114        }
115        "arch" => {
116            if rest.is_empty() {
117                return Err(ShorthandError::MissingArch);
118            }
119            Ok(Rule::Os {
120                action,
121                os: OsConstraint {
122                    name: None,
123                    version: None,
124                    arch: Some(parse_arch(rest)?),
125                },
126            })
127        }
128        other => Err(ShorthandError::UnknownRuleType(other.to_owned())),
129    }
130}
131
132fn os_name_str(n: OsName) -> &'static str {
133    match n {
134        OsName::Linux => "linux",
135        OsName::Windows => "windows",
136        OsName::Osx => "osx",
137    }
138}
139
140fn arch_str(a: OsArch) -> &'static str {
141    match a {
142        OsArch::X86 => "x86",
143        OsArch::X86_64 => "x86_64",
144        OsArch::Arm => "arm",
145        OsArch::Aarch64 => "aarch64",
146        OsArch::Any => "any",
147    }
148}
149
150pub fn encode_short_rule(rule: &Rule) -> RawSingle {
151    let action = match rule.action() {
152        RuleAction::Allow => "allow",
153        RuleAction::Disallow => "disallow",
154    };
155    match rule {
156        Rule::Os { os, .. } => {
157            if let Some(name) = os.name {
158                if let Some(ver) = &os.version {
159                    return RawSingle::Short(format!(
160                        "{action}.os.{}@{ver}",
161                        os_name_str(name)
162                    ));
163                }
164                return RawSingle::Short(format!("{action}.os.{}", os_name_str(name)));
165            }
166            if let Some(arch) = os.arch {
167                return RawSingle::Short(format!("{action}.arch.{}", arch_str(arch)));
168            }
169            RawSingle::Expanded(rule.clone())
170        }
171        Rule::Features { features, .. } => {
172            if features.len() == 1 {
173                let (k, _) = features.iter().next().unwrap();
174                return RawSingle::Short(format!("{action}.features.{k}"));
175            }
176            RawSingle::Expanded(rule.clone())
177        }
178        Rule::Plain { .. } => RawSingle::Short(action.to_owned()),
179    }
180}
181
182pub fn parse_short_ruleset(raw: RawRuleset) -> Result<Ruleset, ShorthandError> {
183    let arr = match raw {
184        RawRuleset::Many(v) => v,
185        RawRuleset::One(s) => vec![s],
186    };
187    arr.into_iter().map(parse_short_rule).collect()
188}
189
190pub fn encode_short_ruleset(ruleset: &Ruleset) -> RawRuleset {
191    let encoded: Vec<RawSingle> = ruleset.iter().map(encode_short_rule).collect();
192    if encoded.len() == 1 {
193        RawRuleset::One(encoded.into_iter().next().unwrap())
194    } else {
195        RawRuleset::Many(encoded)
196    }
197}