supreme_hook_config/
script_engine.rs1use crate::{ModParameter, ParameterKind};
9use rhai::{Engine, Scope};
10use std::collections::HashMap;
11
12#[derive(Debug, thiserror::Error)]
13pub enum ScriptError {
14 #[error("Compile error: {message}")]
15 Compile { message: String },
16 #[error("Runtime error: {message}")]
17 Runtime { message: String },
18 #[error("Unknown variable: {name}")]
19 UnknownVariable { name: String },
20 #[error("Invalid return type: expected {expected}, got {actual}")]
21 InvalidReturnType {
22 expected: &'static str,
23 actual: String,
24 },
25 #[error("Encoding error: {message}")]
26 Encoding { message: String },
27 #[error("Execution limit exceeded: {message}")]
28 ExecutionLimit { message: String },
29}
30
31#[derive(Debug, thiserror::Error)]
32pub enum PatchError {
33 #[error("Script error: {0}")]
34 Script(#[from] ScriptError),
35 #[error("Encoding error: {0}")]
36 Encoding(String),
37 #[error("Invalid patch: {0}")]
38 InvalidPatch(String),
39}
40
41fn create_engine() -> Engine {
43 let mut engine = Engine::new();
44
45 engine.disable_symbol("eval");
47
48 engine.set_max_expr_depths(50, 50);
50 engine.set_max_call_levels(30);
51 engine.set_max_operations(10_000_000);
52
53 engine.register_fn("url_encode", |s: &str| -> String {
58 let mut out = String::with_capacity(s.len());
59 for byte in s.bytes() {
60 match byte {
61 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
62 out.push(byte as char);
63 }
64 _ => {
65 out.push_str(&format!("%{byte:02X}"));
66 }
67 }
68 }
69 out
70 });
71
72 engine.on_print(|x| log::debug!(target: "rhai", "{x}"));
74 engine.on_debug(|x, _, _| log::trace!(target: "rhai", "{x:?}"));
75
76 engine
77}
78
79fn populate_scope(
86 scope: &mut Scope,
87 parameters: &[ModParameter],
88 configuration: &HashMap<String, serde_json::Value>,
89) {
90 for param in parameters {
91 let value = configuration
92 .get(¶m.id)
93 .cloned()
94 .unwrap_or_else(|| match param.kind {
95 ParameterKind::Boolean => serde_json::json!(false),
96 ParameterKind::Dropdown { .. } | ParameterKind::Text => serde_json::json!(""),
97 ParameterKind::Number { .. } => serde_json::json!(0.0),
98 });
99
100 match param.kind {
101 ParameterKind::Boolean => {
102 scope.push(¶m.id, value.as_bool().unwrap_or(false));
103 }
104 ParameterKind::Dropdown { .. } | ParameterKind::Text => {
105 scope.push(¶m.id, value.as_str().unwrap_or("").to_string());
106 }
107 ParameterKind::Number { .. } => {
108 scope.push(¶m.id, value.as_f64().unwrap_or(0.0));
109 }
110 }
111 }
112}
113
114pub fn evaluate_bool_expression(
116 expression: &str,
117 parameters: &[ModParameter],
118 configuration: &HashMap<String, serde_json::Value>,
119) -> Result<bool, ScriptError> {
120 let engine = create_engine();
121 let mut scope = Scope::new();
122 populate_scope(&mut scope, parameters, configuration);
123
124 engine
125 .eval_expression_with_scope::<bool>(&mut scope, expression)
126 .map_err(|e| ScriptError::Runtime {
127 message: e.to_string(),
128 })
129}
130
131pub fn validate_expression(
133 expression: &str,
134 parameters: &[ModParameter],
135) -> Result<(), ScriptError> {
136 let engine = create_engine();
137 let mut scope = Scope::new();
138 populate_scope(&mut scope, parameters, &HashMap::new());
139
140 engine
141 .compile_expression(expression)
142 .map_err(|e| ScriptError::Compile {
143 message: e.to_string(),
144 })?;
145
146 engine
147 .eval_expression_with_scope::<bool>(&mut scope, expression)
148 .map_err(|e| {
149 let err_str = e.to_string();
150 if err_str.contains("Variable not found") {
151 ScriptError::UnknownVariable {
152 name: expression.to_string(),
153 }
154 } else {
155 ScriptError::Runtime { message: err_str }
156 }
157 })?;
158 Ok(())
159}
160
161pub fn run_script_patch(
173 script: &str,
174 input: &str,
175 rel_key: &str,
176 parameters: &[ModParameter],
177 configuration: &HashMap<String, serde_json::Value>,
178) -> Result<String, ScriptError> {
179 let engine = create_engine();
180 let mut scope = Scope::new();
181 scope.push("input", input.to_string());
182 scope.push("file_path", rel_key.to_string());
183 let timestamp: i64 = std::time::SystemTime::now()
184 .duration_since(std::time::UNIX_EPOCH)
185 .unwrap_or_default()
186 .as_nanos() as i64;
187 scope.push("timestamp", timestamp);
188 populate_scope(&mut scope, parameters, configuration);
189
190 let result: String = engine.eval_with_scope(&mut scope, script).map_err(|e| {
191 let err_str = e.to_string();
192 if err_str.contains("Output type incorrect") {
193 ScriptError::InvalidReturnType {
194 expected: "string",
195 actual: "unknown".to_string(),
196 }
197 } else {
198 ScriptError::Runtime { message: err_str }
199 }
200 })?;
201
202 Ok(result)
203}
204
205pub fn validate_script(script: &str, parameters: &[ModParameter]) -> Result<(), ScriptError> {
207 let engine = create_engine();
208 let mut scope = Scope::new();
209 scope.push("input", String::new());
210 populate_scope(&mut scope, parameters, &HashMap::new());
211
212 engine.compile(script).map_err(|e| ScriptError::Compile {
213 message: e.to_string(),
214 })?;
215
216 Ok(())
217}
218
219pub(crate) fn interpolate_string(
232 text: &str,
233 parameters: &[ModParameter],
234 configuration: &HashMap<String, serde_json::Value>,
235) -> Result<String, ScriptError> {
236 let engine = create_engine();
237 let mut scope = Scope::new();
238 populate_scope(&mut scope, parameters, configuration);
239
240 let escaped_text = text.replace('\\', "\\\\").replace('`', "\\`");
245 let script = format!("`{}`", escaped_text);
246
247 engine
248 .eval_with_scope::<String>(&mut scope, &script)
249 .map_err(|e| {
250 let err_str = e.to_string();
251 if err_str.contains("Variable not found") {
252 ScriptError::UnknownVariable {
253 name: text.to_string(),
254 }
255 } else {
256 ScriptError::Runtime { message: err_str }
257 }
258 })
259}