use std::sync::atomic::Ordering;
use base64::Engine as _;
use color_eyre::eyre::{bail, eyre};
use rhai::{AST, Array, Blob, Dynamic, Engine, Scope};
use serde::{Deserialize, Serialize};
use crate::{
ResourceRef,
deceit::DeceitResponseContext,
jinja::{MiniJinjaState, build_tpl_context},
rhai::{RhaiResponseContext, RhaiState},
};
#[derive(Default, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputType {
#[default]
String,
Jinja,
Hex,
Base64,
Rhai,
RhaiRef {
id: String,
#[serde(default)]
args: Vec<String>,
},
}
pub fn output_response_body(
deceit_ref: &ResourceRef,
tp: &OutputType,
output: &str,
ctx: &DeceitResponseContext,
mini_jinja_state: &MiniJinjaState,
rhai_state: &RhaiState,
) -> color_eyre::Result<Vec<u8>> {
match tp {
OutputType::String => Ok(output.as_bytes().to_vec()),
OutputType::Jinja => render_using_minijinja(deceit_ref, output, ctx, mini_jinja_state),
OutputType::Hex => {
let hex_str = output.trim().strip_prefix("0x").unwrap_or(output).trim();
Ok(hex::decode(hex_str)?)
}
OutputType::Base64 => Ok(base64::prelude::BASE64_STANDARD.decode(output.trim())?),
OutputType::Rhai => render_using_rhai(deceit_ref, output, ctx, rhai_state),
OutputType::RhaiRef { id, args } => {
render_using_rhai_ref(deceit_ref, id, args.clone(), ctx, rhai_state)
}
}
}
fn render_using_minijinja(
deceit_ref: &ResourceRef,
template: &str,
ctx: &DeceitResponseContext,
mini_jinja_state: &MiniJinjaState,
) -> color_eyre::Result<Vec<u8>> {
let id = deceit_ref.to_resource_id("jinja-output");
mini_jinja_state.add_minijinja_template(&id, template)?;
let mut env = mini_jinja_state.get_minijinja();
let force_response_code = ctx.response_code.clone();
env.add_function("force_response_code", move |code: u16| {
force_response_code.store(code, Ordering::Relaxed);
});
let tpl = env.get_template(&id)?;
let jinja_ctx = build_tpl_context(ctx.clone());
let response = tpl
.render(jinja_ctx)
.map_err(|e| eyre!("Can't render minijinja template: {e}"))?;
Ok(response.into_bytes())
}
fn render_using_rhai_ref(
rref: &ResourceRef,
script_id: &str,
args: Vec<String>,
ctx: &DeceitResponseContext,
rhai: &RhaiState,
) -> color_eyre::Result<Vec<u8>> {
let (engine, ast) = match rhai.get_exec_global(script_id) {
Ok(lfn) => lfn,
Err(e) => {
log::error!("Can't load Rhai top level scrip by id:{script_id} path:{rref} {e:?}");
return Err(e.into());
}
};
let args = args.into_iter().map(Into::into).collect();
call_rhai(&engine, &ast, ctx.clone().into(), args)
}
fn render_using_rhai(
deceit_ref: &ResourceRef,
script: &str,
ctx: &DeceitResponseContext,
rhai: &RhaiState,
) -> color_eyre::Result<Vec<u8>> {
let id = deceit_ref.to_resource_id("rhai-output");
let (engine, ast) = rhai
.get_exec(id.clone(), script)
.map_err(|e| eyre!("Can't load Rhai template: {e:?}"))?;
call_rhai(&engine, &ast, ctx.clone().into(), Array::new())
}
fn call_rhai(
engine: &Engine,
ast: &AST,
ctx: RhaiResponseContext,
args: Array,
) -> color_eyre::Result<Vec<u8>> {
let mut scope = Scope::new();
scope.set_value("ctx", ctx);
scope.set_value("args", args);
let result = engine.eval_ast_with_scope::<Dynamic>(&mut scope, ast)?;
let value = if result.is_unit() {
Default::default()
} else if result.is_blob() {
result
.try_cast_result::<Blob>()
.map_err(|e| eyre!("Must not happen here {e:?}"))?
} else {
bail!("Wrong Rhai template return type: {}", result.type_name());
};
Ok(value)
}