use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScriptLanguage {
Python,
JavaScript,
Go,
Perl,
}
impl ScriptLanguage {
pub fn command(&self) -> &'static str {
match self {
ScriptLanguage::Python => "python3",
ScriptLanguage::JavaScript => "node",
ScriptLanguage::Go => "go",
ScriptLanguage::Perl => "perl",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"python" | "python3" => Some(ScriptLanguage::Python),
"javascript" | "js" | "node" => Some(ScriptLanguage::JavaScript),
"go" | "golang" => Some(ScriptLanguage::Go),
"perl" => Some(ScriptLanguage::Perl),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
ScriptLanguage::Python => "python",
ScriptLanguage::JavaScript => "javascript",
ScriptLanguage::Go => "go",
ScriptLanguage::Perl => "perl",
}
}
pub fn all_languages() -> Vec<Self> {
vec![
ScriptLanguage::Python,
ScriptLanguage::JavaScript,
ScriptLanguage::Go,
ScriptLanguage::Perl,
]
}
pub fn all_languages_formatted() -> String {
let all = Self::all_languages();
let count = all.len();
if count == 0 {
return String::new();
}
if count == 1 {
return format!("'{}'", all[0].as_str());
}
let mut result = String::new();
for (i, lang) in all.iter().enumerate() {
if i == count - 1 {
result.push_str(&format!("or '{}'", lang.as_str()));
} else if i == count - 2 {
result.push_str(&format!("'{}', ", lang.as_str()));
} else {
result.push_str(&format!("'{}', ", lang.as_str()));
}
}
result
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScriptSource {
FilePath(String),
Inline(String),
}
impl ScriptSource {
pub const MAX_CODE_BYTES: usize = 4 * 1024 * 1024;
pub fn get_code(&self) -> Result<String, std::io::Error> {
match self {
ScriptSource::FilePath(path) => {
use std::io::Read;
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NONBLOCK);
}
let file = options.open(path)?;
if !file.metadata()?.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"script source must be a regular file",
));
}
let mut code = String::new();
file.take(Self::MAX_CODE_BYTES as u64 + 1)
.read_to_string(&mut code)?;
Self::check_size(&code)?;
Ok(code)
}
ScriptSource::Inline(code) => {
Self::check_size(code)?;
Ok(code.clone())
}
}
}
fn check_size(code: &str) -> std::io::Result<()> {
if code.len() > Self::MAX_CODE_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"script source exceeds the 4 MiB cap",
));
}
Ok(())
}
pub async fn get_code_async(&self) -> std::io::Result<String> {
if let Self::Inline(_) = self {
return self.get_code();
}
#[cfg(target_arch = "wasm32")]
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"file-backed scripts are not available in the browser",
));
#[cfg(not(target_arch = "wasm32"))]
{
let source = self.clone();
tokio::task::spawn_blocking(move || source.get_code())
.await
.map_err(std::io::Error::other)?
}
}
}
#[derive(Debug, Clone)]
pub struct ScriptConfig {
pub language: ScriptLanguage,
pub source: ScriptSource,
pub handles_contexts: Vec<String>,
}
impl ScriptConfig {
pub fn handles_context(&self, event_type_id: &str) -> bool {
self.handles_contexts
.iter()
.any(|context| context == "all" || context == event_type_id)
}
pub fn add_contexts(&mut self, contexts: Vec<String>) {
for context in contexts {
if !self.handles_contexts.contains(&context) {
self.handles_contexts.push(context);
}
}
if self.handles_contexts.contains(&"all".to_string()) {
self.handles_contexts = vec!["all".to_string()];
}
}
pub fn remove_contexts(&mut self, contexts: &[String]) {
self.handles_contexts.retain(|c| !contexts.contains(c));
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptInput {
pub event_type_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<ServerContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client: Option<ClientContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection: Option<ConnectionContext>,
pub event: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerContext {
pub id: u32,
pub port: u16,
pub stack: String,
pub memory: String,
pub instruction: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientContext {
pub id: u32,
pub remote_addr: String,
pub protocol: String,
pub memory: String,
pub instruction: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionContext {
pub id: String,
pub remote_addr: String,
pub bytes_received: u64,
pub bytes_sent: u64,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ScriptResponse {
pub actions: Vec<serde_json::Value>,
}
pub fn parse_script_response(s: &str) -> anyhow::Result<ScriptResponse> {
if let Ok(response) = serde_json::from_str::<ScriptResponse>(s) {
return Ok(response);
}
if let Ok(actions) = serde_json::from_str::<Vec<serde_json::Value>>(s) {
return Ok(ScriptResponse { actions });
}
anyhow::bail!(
"Failed to parse script response. Expected either {{\"actions\": [...]}} or [...]. Input: {}",
s
)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScriptUpdateOperation {
Set,
AddContexts,
RemoveContexts,
Disable,
}
impl ScriptUpdateOperation {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"set" => Some(ScriptUpdateOperation::Set),
"add_contexts" | "add" => Some(ScriptUpdateOperation::AddContexts),
"remove_contexts" | "remove" => Some(ScriptUpdateOperation::RemoveContexts),
"disable" | "clear" => Some(ScriptUpdateOperation::Disable),
_ => None,
}
}
}