1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum ScriptLanguage {
9 Python,
10 JavaScript,
11 Go,
12 Perl,
13}
14
15impl ScriptLanguage {
16 pub fn command(&self) -> &'static str {
18 match self {
19 ScriptLanguage::Python => "python3",
20 ScriptLanguage::JavaScript => "node",
21 ScriptLanguage::Go => "go",
22 ScriptLanguage::Perl => "perl",
23 }
24 }
25
26 pub fn parse(s: &str) -> Option<Self> {
28 match s.to_lowercase().as_str() {
29 "python" | "python3" => Some(ScriptLanguage::Python),
30 "javascript" | "js" | "node" => Some(ScriptLanguage::JavaScript),
31 "go" | "golang" => Some(ScriptLanguage::Go),
32 "perl" => Some(ScriptLanguage::Perl),
33 _ => None,
34 }
35 }
36
37 pub fn as_str(&self) -> &'static str {
39 match self {
40 ScriptLanguage::Python => "python",
41 ScriptLanguage::JavaScript => "javascript",
42 ScriptLanguage::Go => "go",
43 ScriptLanguage::Perl => "perl",
44 }
45 }
46
47 pub fn all_languages() -> Vec<Self> {
49 vec![
50 ScriptLanguage::Python,
51 ScriptLanguage::JavaScript,
52 ScriptLanguage::Go,
53 ScriptLanguage::Perl,
54 ]
55 }
56
57 pub fn all_languages_formatted() -> String {
60 let all = Self::all_languages();
61 let count = all.len();
62
63 if count == 0 {
64 return String::new();
65 }
66
67 if count == 1 {
68 return format!("'{}'", all[0].as_str());
69 }
70
71 let mut result = String::new();
72 for (i, lang) in all.iter().enumerate() {
73 if i == count - 1 {
74 result.push_str(&format!("or '{}'", lang.as_str()));
76 } else if i == count - 2 {
77 result.push_str(&format!("'{}', ", lang.as_str()));
79 } else {
80 result.push_str(&format!("'{}', ", lang.as_str()));
82 }
83 }
84 result
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum ScriptSource {
91 FilePath(String),
93 Inline(String),
95}
96
97impl ScriptSource {
98 pub const MAX_CODE_BYTES: usize = 4 * 1024 * 1024;
100
101 pub fn get_code(&self) -> Result<String, std::io::Error> {
103 match self {
104 ScriptSource::FilePath(path) => {
105 use std::io::Read;
106 let mut options = std::fs::OpenOptions::new();
107 options.read(true);
108 #[cfg(unix)]
109 {
110 use std::os::unix::fs::OpenOptionsExt;
111 options.custom_flags(libc::O_NONBLOCK);
114 }
115 let file = options.open(path)?;
116 if !file.metadata()?.is_file() {
117 return Err(std::io::Error::new(
118 std::io::ErrorKind::InvalidInput,
119 "script source must be a regular file",
120 ));
121 }
122 let mut code = String::new();
123 file.take(Self::MAX_CODE_BYTES as u64 + 1)
124 .read_to_string(&mut code)?;
125 Self::check_size(&code)?;
126 Ok(code)
127 }
128 ScriptSource::Inline(code) => {
129 Self::check_size(code)?;
130 Ok(code.clone())
131 }
132 }
133 }
134
135 fn check_size(code: &str) -> std::io::Result<()> {
136 if code.len() > Self::MAX_CODE_BYTES {
137 return Err(std::io::Error::new(
138 std::io::ErrorKind::InvalidData,
139 "script source exceeds the 4 MiB cap",
140 ));
141 }
142 Ok(())
143 }
144
145 pub async fn get_code_async(&self) -> std::io::Result<String> {
147 if let Self::Inline(_) = self {
148 return self.get_code();
149 }
150 #[cfg(target_arch = "wasm32")]
151 return Err(std::io::Error::new(
152 std::io::ErrorKind::Unsupported,
153 "file-backed scripts are not available in the browser",
154 ));
155 #[cfg(not(target_arch = "wasm32"))]
156 {
157 let source = self.clone();
158 tokio::task::spawn_blocking(move || source.get_code())
159 .await
160 .map_err(std::io::Error::other)?
161 }
162 }
163}
164
165#[derive(Debug, Clone)]
167pub struct ScriptConfig {
168 pub language: ScriptLanguage,
170
171 pub source: ScriptSource,
173
174 pub handles_contexts: Vec<String>,
176}
177
178impl ScriptConfig {
179 pub fn handles_context(&self, event_type_id: &str) -> bool {
181 self.handles_contexts
182 .iter()
183 .any(|context| context == "all" || context == event_type_id)
184 }
185
186 pub fn add_contexts(&mut self, contexts: Vec<String>) {
188 for context in contexts {
189 if !self.handles_contexts.contains(&context) {
190 self.handles_contexts.push(context);
191 }
192 }
193 if self.handles_contexts.contains(&"all".to_string()) {
195 self.handles_contexts = vec!["all".to_string()];
196 }
197 }
198
199 pub fn remove_contexts(&mut self, contexts: &[String]) {
201 self.handles_contexts.retain(|c| !contexts.contains(c));
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ScriptInput {
208 pub event_type_id: String,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
215 pub server: Option<ServerContext>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub client: Option<ClientContext>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub connection: Option<ConnectionContext>,
224
225 pub event: serde_json::Value,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ServerContext {
232 pub id: u32,
234
235 pub port: u16,
237
238 pub stack: String,
240
241 pub memory: String,
243
244 pub instruction: String,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ClientContext {
251 pub id: u32,
253
254 pub remote_addr: String,
256
257 pub protocol: String,
259
260 pub memory: String,
262
263 pub instruction: String,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct ConnectionContext {
270 pub id: String,
272
273 pub remote_addr: String,
275
276 pub bytes_received: u64,
278
279 pub bytes_sent: u64,
281}
282
283#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
285pub struct ScriptResponse {
286 pub actions: Vec<serde_json::Value>,
287}
288
289pub fn parse_script_response(s: &str) -> anyhow::Result<ScriptResponse> {
294 if let Ok(response) = serde_json::from_str::<ScriptResponse>(s) {
296 return Ok(response);
297 }
298
299 if let Ok(actions) = serde_json::from_str::<Vec<serde_json::Value>>(s) {
301 return Ok(ScriptResponse { actions });
302 }
303
304 anyhow::bail!(
306 "Failed to parse script response. Expected either {{\"actions\": [...]}} or [...]. Input: {}",
307 s
308 )
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "snake_case")]
314pub enum ScriptUpdateOperation {
315 Set,
317
318 AddContexts,
320
321 RemoveContexts,
323
324 Disable,
326}
327
328impl ScriptUpdateOperation {
329 pub fn parse(s: &str) -> Option<Self> {
331 match s.to_lowercase().as_str() {
332 "set" => Some(ScriptUpdateOperation::Set),
333 "add_contexts" | "add" => Some(ScriptUpdateOperation::AddContexts),
334 "remove_contexts" | "remove" => Some(ScriptUpdateOperation::RemoveContexts),
335 "disable" | "clear" => Some(ScriptUpdateOperation::Disable),
336 _ => None,
337 }
338 }
339}