use bytes::Bytes;
use fastly::http::Method;
use fastly::Request;
use regex::RegexBuilder;
use std::{borrow::Cow, cell::RefCell, collections::HashMap, fmt::Display, rc::Rc};
use crate::{
element_handler::{ElementHandler, Flow},
functions,
literals::*,
parser_types::{Element, Expr, IncludeAttributes, Operator},
ESIError, Result,
};
#[derive(Debug, Clone, Default)]
pub struct FunctionRegistry {
functions: HashMap<String, Vec<Element>>,
}
impl FunctionRegistry {
pub fn new() -> Self {
Self {
functions: HashMap::new(),
}
}
pub fn register(&mut self, name: String, body: Vec<Element>) {
self.functions.insert(name, body);
}
pub fn get(&self, name: &str) -> Option<&Vec<Element>> {
self.functions.get(name)
}
}
pub fn eval_expr(expr: &Expr, ctx: &mut EvalContext) -> Result<Value> {
match expr {
Expr::Integer(i) => Ok(Value::Integer(*i)),
Expr::String(Some(b)) => Ok(Value::Text(b.clone())),
Expr::String(None) => Ok(Value::Text(Bytes::new())),
Expr::Variable(name, key, default) => {
let evaluated_key = if let Some(key_expr) = key {
let key_result = eval_expr(key_expr, ctx)?;
Some(key_result.to_string())
} else {
None
};
let value = ctx.get_variable(name, evaluated_key.as_deref());
if matches!(value, Value::Null) {
if let Some(default_expr) = default {
return eval_expr(default_expr, ctx);
}
}
Ok(value)
}
Expr::Comparison {
left,
operator,
right,
} => {
if *operator == Operator::And {
let left_val = eval_expr(left, ctx)?;
if !left_val.to_bool() {
return Ok(Value::Boolean(false));
}
return Ok(Value::Boolean(eval_expr(right, ctx)?.to_bool()));
}
if *operator == Operator::Or {
let left_val = eval_expr(left, ctx)?;
if left_val.to_bool() {
return Ok(Value::Boolean(true));
}
return Ok(Value::Boolean(eval_expr(right, ctx)?.to_bool()));
}
let left_val = eval_expr(left, ctx)?;
let right_val = eval_expr(right, ctx)?;
eval_comparison(&left_val, &right_val, operator, ctx)
}
Expr::Call(func_name, args) => {
let mut values = Vec::with_capacity(args.len());
for arg in args {
values.push(eval_expr(arg, ctx)?);
}
call_dispatch(func_name, &values, ctx)
}
Expr::Not(expr) => {
let inner_value = eval_expr(expr, ctx)?;
Ok(Value::Boolean(!inner_value.to_bool()))
}
Expr::DictLiteral(pairs) => {
let mut map = HashMap::with_capacity(pairs.len());
for (key_expr, val_expr) in pairs {
let key = eval_expr(key_expr, ctx)?;
let val = eval_expr(val_expr, ctx)?;
map.insert(key.to_string(), val);
}
Ok(Value::new_dict(map))
}
Expr::ListLiteral(items) => {
let mut values = Vec::with_capacity(items.len());
for item_expr in items {
values.push(eval_expr(item_expr, ctx)?);
}
Ok(Value::new_list(values))
}
Expr::Interpolated(elements) => {
let mut result = String::new();
for element in elements {
match element {
Element::Content(text) => {
result.push_str(&String::from_utf8_lossy(text.as_ref()));
}
Element::Html(html) => {
result.push_str(&String::from_utf8_lossy(html.as_ref()));
}
Element::Expr(expr) => {
let value = eval_expr(expr, ctx)?;
result.push_str(&value.to_string());
}
Element::Esi(_) => {
}
}
}
Ok(Value::Text(Bytes::from(result)))
}
}
}
fn eval_comparison(
left_val: &Value,
right_val: &Value,
operator: &Operator,
ctx: &mut EvalContext,
) -> Result<Value> {
match operator {
Operator::Range => {
match (left_val, right_val) {
(Value::Integer(start), Value::Integer(end)) => {
let values: Vec<Value> = if start <= end {
(*start..=*end).map(Value::Integer).collect()
} else {
(*end..=*start).rev().map(Value::Integer).collect()
};
Ok(Value::new_list(values))
}
_ => Err(ESIError::ExpressionError(
"Range operator (..) requires integer operands".to_string(),
)),
}
}
Operator::Matches | Operator::MatchesInsensitive => {
let test = left_val.as_cow_str();
let pattern = right_val.as_cow_str();
let re = if *operator == Operator::Matches {
RegexBuilder::new(&pattern).build()?
} else {
RegexBuilder::new(&pattern).case_insensitive(true).build()?
};
if let Some(captures) = re.captures(&test) {
let match_name = ctx.match_name.clone();
let mut idx_buf = String::new();
for (i, cap) in captures.iter().enumerate() {
let capval = cap.map_or(Value::Null, |s| {
Value::Text(Bytes::copy_from_slice(s.as_str().as_bytes()))
});
idx_buf.clear();
use std::fmt::Write;
let _ = write!(idx_buf, "{i}");
ctx.set_variable(&match_name, Some(&idx_buf), capval)?;
}
Ok(Value::Boolean(true))
} else {
Ok(Value::Boolean(false))
}
}
Operator::Has => {
let haystack: &str = &left_val.as_cow_str();
let needle: &str = &right_val.as_cow_str();
Ok(Value::Boolean(haystack.contains(needle)))
}
Operator::HasInsensitive => {
let haystack: String = left_val.as_cow_str().to_lowercase();
let needle: &str = &right_val.as_cow_str().to_lowercase();
Ok(Value::Boolean(haystack.as_str().contains(needle)))
}
Operator::Equals => match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => Ok(Value::Boolean(l == r)),
(Value::Text(l), Value::Text(r)) => Ok(Value::Boolean(l == r)),
_ => Ok(Value::Boolean(
left_val.as_cow_str() == right_val.as_cow_str(),
)),
},
Operator::NotEquals => match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => Ok(Value::Boolean(l != r)),
(Value::Text(l), Value::Text(r)) => Ok(Value::Boolean(l != r)),
_ => Ok(Value::Boolean(
left_val.as_cow_str() != right_val.as_cow_str(),
)),
},
Operator::LessThan => match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => Ok(Value::Boolean(l < r)),
(Value::Text(l), Value::Text(r)) => Ok(Value::Boolean(l < r)),
_ => Ok(Value::Boolean(
left_val.as_cow_str() < right_val.as_cow_str(),
)),
},
Operator::LessThanOrEqual => match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => Ok(Value::Boolean(l <= r)),
(Value::Text(l), Value::Text(r)) => Ok(Value::Boolean(l <= r)),
_ => Ok(Value::Boolean(
left_val.as_cow_str() <= right_val.as_cow_str(),
)),
},
Operator::GreaterThan => match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => Ok(Value::Boolean(l > r)),
(Value::Text(l), Value::Text(r)) => Ok(Value::Boolean(l > r)),
_ => Ok(Value::Boolean(
left_val.as_cow_str() > right_val.as_cow_str(),
)),
},
Operator::GreaterThanOrEqual => match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => Ok(Value::Boolean(l >= r)),
(Value::Text(l), Value::Text(r)) => Ok(Value::Boolean(l >= r)),
_ => Ok(Value::Boolean(
left_val.as_cow_str() >= right_val.as_cow_str(),
)),
},
Operator::And | Operator::Or => {
unreachable!("And/Or are short-circuit evaluated in eval_expr")
}
Operator::Add => {
match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => l.checked_add(*r).map_or_else(
|| {
Err(ESIError::ExpressionError(format!(
"Integer overflow in addition: {l} + {r}"
)))
},
|result| Ok(Value::Integer(result)),
),
(Value::List(a), Value::List(b)) => {
let mut items = a.borrow().clone();
items.extend(b.borrow().iter().cloned());
Ok(Value::new_list(items))
}
_ => {
let result = format!("{left_val}{right_val}");
Ok(Value::Text(Bytes::from(result)))
}
}
}
Operator::Subtract => {
if let (Value::Integer(l), Value::Integer(r)) = (left_val, right_val) {
l.checked_sub(*r).map_or_else(
|| {
Err(ESIError::ExpressionError(format!(
"Integer overflow in subtraction: {l} - {r}"
)))
},
|result| Ok(Value::Integer(result)),
)
} else {
Err(ESIError::ExpressionError(format!(
"Subtraction requires numeric operands, got {left_val} - {right_val}"
)))
}
}
Operator::Multiply => {
match (left_val, right_val) {
(Value::Integer(l), Value::Integer(r)) => l.checked_mul(*r).map_or_else(
|| {
Err(ESIError::ExpressionError(format!(
"Integer overflow in multiplication: {l} * {r}"
)))
},
|result| Ok(Value::Integer(result)),
),
(Value::Integer(n), Value::Text(s)) | (Value::Text(s), Value::Integer(n)) => {
if *n < 0 {
Err(ESIError::ExpressionError(format!(
"String repetition count must be non-negative, got {n}"
)))
} else {
let text = String::from_utf8_lossy(s.as_ref());
let result = text.repeat(*n as usize);
Ok(Value::Text(Bytes::from(result)))
}
}
(Value::Integer(n), Value::List(items))
| (Value::List(items), Value::Integer(n)) => {
if *n < 0 {
Err(ESIError::ExpressionError(format!(
"List repetition count must be non-negative, got {n}"
)))
} else {
let borrowed = items.borrow();
let mut result = Vec::with_capacity(borrowed.len() * (*n as usize));
for _ in 0..*n {
result.extend(borrowed.iter().cloned());
}
Ok(Value::new_list(result))
}
}
_ => Err(ESIError::ExpressionError(format!(
"Multiplication requires numeric operands, or integer with string/list, got {left_val} * {right_val}"
))),
}
}
Operator::Divide => {
if let (Value::Integer(l), Value::Integer(r)) = (left_val, right_val) {
if *r == 0 {
Err(ESIError::ExpressionError(format!(
"Division by zero: {l} / 0"
)))
} else {
Ok(Value::Integer(l / r))
}
} else {
Err(ESIError::ExpressionError(format!(
"Division requires numeric operands, got {left_val} / {right_val}"
)))
}
}
Operator::Modulo => {
if let (Value::Integer(l), Value::Integer(r)) = (left_val, right_val) {
if *r == 0 {
Err(ESIError::ExpressionError(format!(
"Modulo by zero: {l} % 0"
)))
} else {
Ok(Value::Integer(l % r))
}
} else {
Err(ESIError::ExpressionError(format!(
"Modulo requires numeric operands, got {left_val} % {right_val}"
)))
}
}
}
}
pub struct EvalContext {
vars: HashMap<String, Value>,
match_name: String,
request: Request,
response_headers: Vec<(String, String)>,
last_rand: Option<i32>,
response_status: Option<i32>,
response_body_override: Option<Bytes>,
query_params_cache: std::cell::RefCell<Option<HashMap<String, Vec<Bytes>>>>,
http_headers_cache: std::cell::RefCell<HashMap<String, Option<HashMap<String, Value>>>>,
min_ttl: Option<u32>,
is_uncacheable: bool,
args_stack: Vec<Vec<Value>>,
function_registry: FunctionRegistry,
function_recursion_depth: usize,
}
impl Default for EvalContext {
fn default() -> Self {
Self {
vars: HashMap::new(),
match_name: VAR_MATCHES.to_string(),
request: Request::new(Method::GET, URL_LOCALHOST),
response_headers: Vec::new(),
last_rand: None,
response_status: None,
response_body_override: None,
query_params_cache: std::cell::RefCell::new(None),
http_headers_cache: std::cell::RefCell::new(HashMap::new()),
min_ttl: None,
is_uncacheable: false,
args_stack: Vec::new(),
function_registry: FunctionRegistry::new(),
function_recursion_depth: 5,
}
}
}
impl EvalContext {
pub fn new() -> Self {
Self::default()
}
pub fn new_with_vars(vars: HashMap<String, Value>) -> Self {
Self {
vars,
..Self::default()
}
}
pub fn add_response_header(&mut self, name: String, value: String) {
self.response_headers.push((name, value));
}
pub const fn set_last_rand(&mut self, v: i32) {
self.last_rand = Some(v);
}
pub const fn last_rand(&self) -> Option<i32> {
self.last_rand
}
pub fn response_headers(&self) -> &[(String, String)] {
&self.response_headers
}
pub const fn set_response_status(&mut self, status: i32) {
self.response_status = Some(status);
}
pub const fn response_status(&self) -> Option<i32> {
self.response_status
}
pub fn set_response_body_override(&mut self, body: Option<Bytes>) {
self.response_body_override = body;
}
pub const fn response_body_override(&self) -> Option<&Bytes> {
self.response_body_override.as_ref()
}
fn parse_query_params(&self) -> HashMap<String, Vec<Bytes>> {
let mut params: HashMap<String, Vec<Bytes>> = HashMap::new();
if let Some(query) = self.request.get_query_str() {
for pair in query.split('&') {
if let Some((key, value)) = pair.split_once('=') {
params
.entry(key.to_string())
.or_default()
.push(Bytes::from(value.to_string()));
} else if !pair.is_empty() {
params
.entry(pair.to_string())
.or_default()
.push(Bytes::new());
}
}
}
params
}
fn get_query_params(&self) -> std::cell::Ref<'_, Option<HashMap<String, Vec<Bytes>>>> {
if self.query_params_cache.borrow().is_none() {
*self.query_params_cache.borrow_mut() = Some(self.parse_query_params());
}
self.query_params_cache.borrow()
}
fn parse_http_header(&self, header: &str) -> Option<HashMap<String, Value>> {
let value = self.request.get_header(header)?.to_str().ok()?;
if header.eq_ignore_ascii_case("cookie") {
let mut dict = HashMap::new();
for pair in value.split(';') {
let trimmed = pair.trim();
if let Some((k, v)) = trimmed.split_once('=') {
dict.insert(
k.trim().to_string(),
Value::Text(v.trim().to_owned().into()),
);
}
}
return if dict.is_empty() { None } else { Some(dict) };
}
let mut dict = HashMap::new();
for item in value.split(',') {
let item_value = item.split(';').next().unwrap_or("").trim();
if !item_value.is_empty() {
dict.insert(
item_value.to_string(),
Value::Text(item_value.to_owned().into()),
);
}
}
if dict.is_empty() {
None } else {
Some(dict)
}
}
fn get_http_header_dict(
&self,
header: &str,
) -> std::cell::Ref<'_, HashMap<String, Option<HashMap<String, Value>>>> {
if !self.http_headers_cache.borrow().contains_key(header) {
let parsed = self.parse_http_header(header);
self.http_headers_cache
.borrow_mut()
.insert(header.to_string(), parsed);
}
self.http_headers_cache.borrow()
}
pub fn get_variable(&self, key: &str, subkey: Option<&str>) -> Value {
match key {
VAR_ARGS => {
self.current_args().map_or_else(
|| Value::Null,
|args| {
subkey.map_or_else(
|| {
Value::new_list(args.clone())
},
|sub| {
sub.parse::<usize>().map_or(Value::Null, |index| {
args.get(index).cloned().unwrap_or(Value::Null)
})
},
)
},
)
}
VAR_REQUEST_METHOD => Value::Text(self.request.get_method_str().to_string().into()),
VAR_REQUEST_PATH => Value::Text(self.request.get_path().to_string().into()),
VAR_REMOTE_ADDR => Value::Text(
self.request
.get_client_ip_addr()
.map_or_else(String::new, |ip| ip.to_string())
.into(),
),
VAR_QUERY_STRING => {
let params_ref = self.get_query_params();
let Some(params) = params_ref.as_ref() else {
return Value::Null;
};
subkey.map_or_else(
|| {
if params.is_empty() {
Value::Null
} else {
let mut dict = HashMap::with_capacity(params.len());
for (key, values) in params {
let value = match values.len() {
0 => Value::Null,
1 => Value::Text(values[0].clone()),
_ => Value::new_list(
values.iter().map(|v| Value::Text(v.clone())).collect(),
),
};
dict.insert(key.clone(), value);
}
Value::new_dict(dict)
}
},
|field| match params.get(field) {
None => Value::Null,
Some(values) if values.is_empty() => Value::Null,
Some(values) if values.len() == 1 => Value::Text(values[0].clone()),
Some(values) => {
Value::new_list(values.iter().map(|v| Value::Text(v.clone())).collect())
}
},
)
}
_ if key.starts_with(VAR_HTTP_PREFIX) => {
let header = key.strip_prefix(VAR_HTTP_PREFIX).unwrap_or_default();
let raw_value = self
.request
.get_header(header)
.and_then(|h| h.to_str().ok())
.unwrap_or("");
if raw_value.is_empty() {
return Value::Null;
}
subkey.map_or_else(
|| {
Value::Text(raw_value.to_owned().into())
},
|field| {
let cache = self.get_http_header_dict(header);
if let Some(Some(dict)) = cache.get(header) {
dict.get(field).cloned().unwrap_or(Value::Null)
} else {
Value::Null
}
},
)
}
_ => {
let stored = self.vars.get(key).cloned().unwrap_or(Value::Null);
match subkey {
None => stored,
Some(sub) => get_subvalue(&stored, sub),
}
}
}
}
pub fn set_variable(&mut self, key: &str, subkey: Option<&str>, value: Value) -> Result<()> {
if matches!(value, Value::Null) {
return Ok(());
}
match subkey {
None => {
self.vars.insert(key.to_string(), value);
Ok(())
}
Some(sub) => {
let entry = self
.vars
.entry(key.to_string())
.or_insert_with(|| Value::new_dict(HashMap::new()));
set_subvalue(entry, sub, value)
}
}
}
pub fn set_match_name(&mut self, match_name: &str) {
self.match_name = match_name.to_string();
}
pub fn set_request(&mut self, request: Request) {
self.request = request;
*self.query_params_cache.borrow_mut() = None;
self.http_headers_cache.borrow_mut().clear();
}
pub const fn get_request(&self) -> &Request {
&self.request
}
pub fn update_cache_min_ttl(&mut self, ttl: u32) {
self.min_ttl = Some(self.min_ttl.map_or(ttl, |current_min| current_min.min(ttl)));
}
pub const fn mark_document_uncacheable(&mut self) {
self.is_uncacheable = true;
}
pub fn cache_control_header(&self, rendered_ttl: Option<u32>) -> Option<String> {
if self.is_uncacheable {
return Some("private, no-cache".to_string());
}
let ttl = rendered_ttl.or(self.min_ttl)?;
Some(format!("public, max-age={ttl}"))
}
pub fn push_args(&mut self, args: Vec<Value>) {
self.args_stack.push(args);
}
pub fn pop_args(&mut self) {
self.args_stack.pop();
}
pub fn current_args(&self) -> Option<&Vec<Value>> {
self.args_stack.last()
}
pub fn register_function(&mut self, name: String, body: Vec<Element>) {
self.function_registry.register(name, body);
}
pub fn get_function(&self, name: &str) -> Option<&Vec<Element>> {
self.function_registry.get(name)
}
pub const fn set_max_function_recursion_depth(&mut self, depth: usize) {
self.function_recursion_depth = depth;
}
}
impl<const N: usize> From<[(String, Value); N]> for EvalContext {
fn from(data: [(String, Value); N]) -> Self {
Self::new_with_vars(HashMap::from(data))
}
}
fn get_subvalue(parent: &Value, subkey: &str) -> Value {
if let Ok(idx) = subkey.parse::<usize>() {
if let Value::List(items) = parent {
return items.borrow().get(idx).cloned().unwrap_or(Value::Null);
}
if let Value::Text(s) = parent {
return if idx < s.len() {
Value::Text(s.slice(idx..=idx))
} else {
Value::Null
};
}
}
if let Value::Dict(map) = parent {
return map.borrow().get(subkey).cloned().unwrap_or(Value::Null);
}
Value::Null
}
fn set_subvalue(parent: &mut Value, subkey: &str, value: Value) -> Result<()> {
if let Ok(idx) = subkey.parse::<usize>() {
match parent {
Value::List(items) => {
let mut items = items.borrow_mut();
if idx >= items.len() {
return Err(ESIError::VariableError(format!(
"list index {} out of range (list has {} elements)",
idx,
items.len()
)));
}
items[idx] = value;
return Ok(());
}
Value::Dict(map) => {
map.borrow_mut().insert(subkey.to_string(), value);
return Ok(());
}
_ => {
return Err(ESIError::VariableError(
"cannot create list on the fly - list must already exist".to_string(),
));
}
}
}
match parent {
Value::Dict(map) => {
map.borrow_mut().insert(subkey.to_string(), value);
Ok(())
}
Value::List(_) => {
Err(ESIError::VariableError(
"cannot assign string key to a list".to_string(),
))
}
_ => {
let mut map = HashMap::new();
map.insert(subkey.to_string(), value);
*parent = Value::new_dict(map);
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub enum Value {
Integer(i32),
Text(Bytes),
Boolean(bool),
List(Rc<RefCell<Vec<Value>>>),
Dict(Rc<RefCell<HashMap<String, Value>>>),
Null,
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Integer(a), Self::Integer(b)) => a == b,
(Self::Text(a), Self::Text(b)) => a == b,
(Self::Boolean(a), Self::Boolean(b)) => a == b,
(Self::List(a), Self::List(b)) => *a.borrow() == *b.borrow(),
(Self::Dict(a), Self::Dict(b)) => *a.borrow() == *b.borrow(),
(Self::Null, Self::Null) => true,
_ => false,
}
}
}
impl Eq for Value {}
impl Value {
pub fn new_list(items: Vec<Self>) -> Self {
Self::List(Rc::new(RefCell::new(items)))
}
pub fn new_dict(map: HashMap<String, Self>) -> Self {
Self::Dict(Rc::new(RefCell::new(map)))
}
pub fn as_i32(&self, ctx: &str) -> Result<i32> {
match self {
Self::Integer(i) => Ok(*i),
Self::Text(b) => atoi::atoi::<i32>(b.as_ref().trim_ascii())
.ok_or_else(|| ESIError::FunctionError(format!("{ctx}: invalid integer"))),
Self::Null => Ok(0),
_ => Err(ESIError::FunctionError(format!("{ctx}: invalid integer"))),
}
}
pub fn as_str(&self, ctx: &str) -> Result<&str> {
if let Self::Text(b) = self {
std::str::from_utf8(b)
.map_err(|_| ESIError::FunctionError(format!("{ctx}: invalid string")))
} else {
Err(ESIError::FunctionError(format!("{ctx}: invalid string")))
}
}
pub(crate) fn to_bool(&self) -> bool {
match self {
&Self::Integer(n) => !matches!(n, 0),
Self::Text(s) => !s.is_empty(),
Self::Boolean(b) => *b,
Self::List(items) => !items.borrow().is_empty(),
Self::Dict(map) => !map.borrow().is_empty(),
&Self::Null => false,
}
}
pub(crate) fn to_bytes(&self) -> Bytes {
match self {
Self::Integer(i) => Bytes::from(i.to_string()),
Self::Text(b) => b.clone(), Self::Boolean(b) => {
if *b {
Bytes::from_static(BOOL_TRUE)
} else {
Bytes::from_static(BOOL_FALSE)
}
}
Self::List(items) => Bytes::from(items_to_string(&items.borrow())),
Self::Dict(map) => Bytes::from(dict_to_string(&map.borrow())),
Self::Null => Bytes::new(),
}
}
pub fn as_cow_str(&self) -> Cow<'_, str> {
match self {
Self::Text(b) => String::from_utf8_lossy(b.as_ref()),
_ => Cow::Owned(self.to_string()),
}
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Self::Text(Bytes::from(s))
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Self::Text(Bytes::copy_from_slice(s.as_bytes()))
}
}
impl From<Bytes> for Value {
fn from(b: Bytes) -> Self {
Self::Text(b)
}
}
impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Integer(i) => write!(f, "{i}"),
Self::Text(b) => write!(f, "{}", String::from_utf8_lossy(b.as_ref())),
Self::Boolean(b) => write!(f, "{}", if *b { "true" } else { "false" }),
Self::List(items) => write!(f, "{}", items_to_string(&items.borrow())),
Self::Dict(map) => write!(f, "{}", dict_to_string(&map.borrow())),
Self::Null => Ok(()), }
}
}
fn items_to_string(items: &[Value]) -> String {
let mut out = String::new();
for (i, v) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&v.as_cow_str());
}
out
}
fn dict_to_string(map: &HashMap<String, Value>) -> String {
let mut parts: Vec<_> = map
.iter()
.map(|(k, v)| format!("{k}={}", v.as_cow_str()))
.collect();
parts.sort();
parts.join("&")
}
struct FunctionHandler<'a> {
ctx: &'a mut EvalContext,
output: &'a mut Vec<u8>,
}
impl ElementHandler for FunctionHandler<'_> {
fn ctx(&mut self) -> &mut EvalContext {
self.ctx
}
fn write_bytes(&mut self, bytes: bytes::Bytes) -> Result<()> {
self.output.extend_from_slice(&bytes);
Ok(())
}
fn on_return(&mut self, value: &Expr) -> Result<Flow> {
let val = eval_expr(value, self.ctx)?;
Ok(Flow::Return(val))
}
fn on_include(&mut self, _attrs: &IncludeAttributes) -> Result<Flow> {
Err(ESIError::FunctionError(
"esi:include is not allowed in function bodies".to_string(),
))
}
fn on_eval(&mut self, _attrs: &IncludeAttributes) -> Result<Flow> {
Err(ESIError::FunctionError(
"esi:eval is not allowed in function bodies".to_string(),
))
}
fn on_try(
&mut self,
_attempt_events: Vec<Vec<Element>>,
_except_events: Vec<Element>,
) -> Result<Flow> {
Ok(Flow::Continue)
}
fn on_function(&mut self, _name: String, _body: Vec<Element>) -> Result<Flow> {
Err(ESIError::FunctionError(
"esi:function is not allowed in function bodies (nested function definitions are not supported)".to_string(),
))
}
}
fn call_user_function(
name: &str,
body: &[Element],
args: &[Value],
ctx: &mut EvalContext,
) -> Result<Value> {
if ctx.args_stack.len() >= ctx.function_recursion_depth {
return Err(ESIError::FunctionError(format!(
"Maximum recursion depth ({}) exceeded for function '{}'",
ctx.function_recursion_depth, name
)));
}
ctx.push_args(args.to_vec());
let result = (|| {
let mut output = Vec::new();
let mut handler = FunctionHandler {
ctx,
output: &mut output,
};
for element in body {
match handler.process(element)? {
Flow::Continue => continue,
Flow::Return(value) => return Ok(value),
Flow::Break => continue, }
}
Ok(Value::Text(Bytes::from(output)))
})();
ctx.pop_args();
result
}
fn call_dispatch(identifier: &str, args: &[Value], ctx: &mut EvalContext) -> Result<Value> {
if let Some(function_body) = ctx.get_function(identifier).cloned() {
return call_user_function(identifier, &function_body, args, ctx);
}
match identifier {
FN_LOWER => functions::lower(args),
FN_UPPER => functions::upper(args),
FN_HTML_ENCODE => functions::html_encode(args),
FN_HTML_DECODE => functions::html_decode(args),
FN_CONVERT_TO_UNICODE => functions::convert_to_unicode(args),
FN_CONVERT_FROM_UNICODE => functions::convert_from_unicode(args),
FN_REPLACE => functions::replace(args),
FN_STR => functions::to_str(args),
FN_LSTRIP => functions::lstrip(args),
FN_RSTRIP => functions::rstrip(args),
FN_STRIP => functions::strip(args),
FN_SUBSTR => functions::substr(args),
FN_DOLLAR => functions::dollar(args),
FN_DQUOTE => functions::dquote(args),
FN_SQUOTE => functions::squote(args),
FN_BASE64_ENCODE => functions::base64_encode(args),
FN_BASE64_DECODE => functions::base64_decode(args),
FN_URL_ENCODE => functions::url_encode(args),
FN_URL_DECODE => functions::url_decode(args),
FN_EXISTS => functions::exists(args),
FN_IS_EMPTY => functions::is_empty(args),
FN_STRING_SPLIT => functions::string_split(args),
FN_JOIN => functions::join(args),
FN_LIST_DELITEM => functions::list_delitem(args),
FN_INT => functions::int(args),
FN_LEN => functions::len(args),
FN_INDEX => functions::index(args),
FN_RINDEX => functions::rindex(args),
FN_DIGEST_MD5 => functions::digest_md5(args),
FN_DIGEST_MD5_HEX => functions::digest_md5_hex(args),
FN_BIN_INT => functions::bin_int(args),
FN_TIME => functions::time(args),
FN_HTTP_TIME => functions::http_time(args),
FN_STRFTIME => functions::strftime(args),
FN_RAND => functions::rand(args, ctx),
FN_LAST_RAND => functions::last_rand(args, ctx),
FN_ADD_HEADER => functions::add_header(args, ctx),
FN_SET_RESPONSE_CODE => functions::set_response_code(args, ctx),
FN_SET_REDIRECT => functions::set_redirect(args, ctx),
_ => Err(ESIError::FunctionError(format!(
"unknown function: {identifier}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn evaluate_expression(raw_expr: &str, ctx: &mut EvalContext) -> Result<Value> {
let (_, expr) = crate::parser::parse_expression(raw_expr)
.map_err(|e| ESIError::ParseError(format!("failed to parse expression: {e}")))?;
eval_expr(&expr, ctx).map_err(|e| {
ESIError::ExpressionError(format!("error occurred during expression evaluation: {e}"))
})
}
#[test]
fn test_eval_matches_comparison() -> Result<()> {
let result = evaluate_expression(
"$(hello) matches '^foo'",
&mut EvalContext::from([("hello".to_string(), Value::Text("foobar".into()))]),
)?;
assert_eq!(result, Value::Boolean(true));
Ok(())
}
#[test]
fn test_eval_matches_i_comparison() -> Result<()> {
let result = evaluate_expression(
"$(hello) matches_i '^foo'",
&mut EvalContext::from([("hello".to_string(), Value::Text("FOOBAR".into()))]),
)?;
assert_eq!(result, Value::Boolean(true));
Ok(())
}
#[test]
fn test_eval_matches_with_captures() -> Result<()> {
let ctx = &mut EvalContext::from([("hello".to_string(), Value::Text("foobar".into()))]);
let result = evaluate_expression("$(hello) matches '^(fo)o'", ctx)?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("$(MATCHES{1})", ctx)?;
assert_eq!(result, Value::Text("fo".into()));
Ok(())
}
#[test]
fn test_eval_matches_with_captures_and_match_name() -> Result<()> {
let ctx = &mut EvalContext::from([("hello".to_string(), Value::Text("foobar".into()))]);
ctx.set_match_name("my_custom_name");
let result = evaluate_expression("$(hello) matches '^(fo)o'", ctx)?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("$(my_custom_name{1})", ctx)?;
assert_eq!(result, Value::Text("fo".into()));
Ok(())
}
#[test]
fn test_eval_matches_comparison_negative() -> Result<()> {
let result = evaluate_expression(
"$(hello) matches '^foo'",
&mut EvalContext::from([("hello".to_string(), Value::Text("nope".into()))]),
)?;
assert_eq!(result, Value::Boolean(false));
Ok(())
}
#[test]
fn test_eval_lower_call() -> Result<()> {
let result = evaluate_expression("$lower('FOO')", &mut EvalContext::new())?;
assert_eq!(result, Value::Text("foo".into()));
Ok(())
}
#[test]
fn test_eval_html_encode_call() -> Result<()> {
let result = evaluate_expression("$html_encode('a > b < c')", &mut EvalContext::new())?;
assert_eq!(result, Value::Text("a > b < c".into()));
Ok(())
}
#[test]
fn test_eval_replace_call() -> Result<()> {
let result = evaluate_expression(
"$replace('abc-def-ghi-', '-', '==')",
&mut EvalContext::new(),
)?;
assert_eq!(result, Value::Text("abc==def==ghi==".into()));
Ok(())
}
#[test]
fn test_eval_replace_call_with_empty_string() -> Result<()> {
let result =
evaluate_expression("$replace('abc-def-ghi-', '-', '')", &mut EvalContext::new())?;
assert_eq!(result, Value::Text("abcdefghi".into()));
Ok(())
}
#[test]
fn test_eval_replace_call_with_count() -> Result<()> {
let result = evaluate_expression(
"$replace('abc-def-ghi-', '-', '==', 2)",
&mut EvalContext::new(),
)?;
assert_eq!(result, Value::Text("abc==def==ghi-".into()));
Ok(())
}
#[test]
fn test_context_nested_vars() {
let mut ctx = EvalContext::new();
ctx.set_variable("foo", Some("bar"), Value::Text("baz".into()))
.unwrap();
assert_eq!(
ctx.get_variable("foo", Some("bar")),
Value::Text("baz".into())
);
ctx.set_variable(
"arr",
None,
Value::new_list(vec![Value::Null, Value::Null, Value::Null]),
)
.unwrap();
ctx.set_variable("arr", Some("0"), Value::Integer(1))
.unwrap();
ctx.set_variable("arr", Some("2"), Value::Integer(3))
.unwrap();
match ctx.get_variable("arr", None) {
Value::List(items) => {
let items = items.borrow();
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Integer(1));
assert_eq!(items[1], Value::Null);
assert_eq!(items[2], Value::Integer(3));
}
other => panic!("Unexpected value: {:?}", other),
}
assert_eq!(ctx.get_variable("arr", Some("1")), Value::Null);
assert_eq!(ctx.get_variable("arr", Some("2")), Value::Integer(3));
}
#[test]
fn test_list_index_out_of_bounds() {
let mut ctx = EvalContext::new();
ctx.set_variable(
"colors",
None,
Value::new_list(vec![
Value::Text("red".into()),
Value::Text("blue".into()),
Value::Text("green".into()),
]),
)
.unwrap();
let result = ctx.set_variable("colors", Some("3"), Value::Text("yellow".into()));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of range"));
}
#[test]
fn test_cannot_assign_string_key_to_list() {
let mut ctx = EvalContext::new();
ctx.set_variable(
"mylist",
None,
Value::new_list(vec![Value::Integer(1), Value::Integer(2)]),
)
.unwrap();
let result = ctx.set_variable("mylist", Some("foo"), Value::Text("bar".into()));
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("cannot assign string key to a list"));
}
#[test]
fn test_dict_created_on_fly() {
let mut ctx = EvalContext::new();
ctx.set_variable("ages", Some("bob"), Value::Integer(34))
.unwrap();
ctx.set_variable("ages", Some("joan"), Value::Integer(28))
.unwrap();
let bob_age = ctx.get_variable("ages", Some("bob"));
assert_eq!(bob_age, Value::Integer(34), "Should retrieve bob's age");
let joan_age = ctx.get_variable("ages", Some("joan"));
assert_eq!(joan_age, Value::Integer(28), "Should retrieve joan's age");
let ages_dict = ctx.get_variable("ages", None);
if let Value::Dict(map) = ages_dict {
let map = map.borrow();
assert_eq!(map.len(), 2, "Dict should have 2 keys");
assert_eq!(map.get("bob"), Some(&Value::Integer(34)));
assert_eq!(map.get("joan"), Some(&Value::Integer(28)));
} else {
panic!("ages should be a Dict, got {:?}", ages_dict);
}
}
#[test]
fn test_eval_get_request_method() -> Result<()> {
let mut ctx = EvalContext::new();
let result = evaluate_expression("$(REQUEST_METHOD)", &mut ctx)?;
assert_eq!(result, Value::Text("GET".into()));
Ok(())
}
#[test]
fn test_nested_lists() -> Result<()> {
let mut ctx = EvalContext::new();
let result = evaluate_expression("[ 'one', [ 'a', 'x', 'c' ], 'three' ]", &mut ctx)?;
match result {
Value::List(items) => {
let items = items.borrow();
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Text("one".into()));
assert_eq!(items[2], Value::Text("three".into()));
match &items[1] {
Value::List(nested) => {
let nested = nested.borrow();
assert_eq!(nested.len(), 3);
assert_eq!(nested[0], Value::Text("a".into()));
assert_eq!(nested[1], Value::Text("x".into()));
assert_eq!(nested[2], Value::Text("c".into()));
}
other => panic!("Expected nested list, got {:?}", other),
}
}
other => panic!("Expected list, got {:?}", other),
}
Ok(())
}
#[test]
fn test_eval_get_request_path() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_request(Request::new(Method::GET, "http://localhost/hello/there"));
let result = evaluate_expression("$(REQUEST_PATH)", &mut ctx)?;
assert_eq!(result, Value::Text("/hello/there".into()));
Ok(())
}
#[test]
fn test_eval_get_request_query() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_request(Request::new(Method::GET, "http://localhost?hello"));
let result = evaluate_expression("$(QUERY_STRING)", &mut ctx)?;
match result {
Value::Dict(map) => {
let map = map.borrow();
assert_eq!(map.len(), 1);
assert_eq!(map.get("hello"), Some(&Value::Text(Bytes::new())));
}
other => panic!("Expected Dict, got {:?}", other),
}
Ok(())
}
#[test]
fn test_eval_get_request_query_field() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_request(Request::new(Method::GET, "http://localhost?hello=goodbye"));
let result = evaluate_expression("$(QUERY_STRING{'hello'})", &mut ctx)?;
assert_eq!(result, Value::Text("goodbye".into()));
let result = evaluate_expression("$(QUERY_STRING{'nonexistent'})", &mut ctx)?;
assert_eq!(result, Value::Null);
Ok(())
}
#[test]
fn test_eval_get_request_query_field_unquoted() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_request(Request::new(Method::GET, "http://localhost?hello=goodbye"));
let result = evaluate_expression("$(QUERY_STRING{hello})", &mut ctx)?;
assert_eq!(result, Value::Text("goodbye".into()));
let result = evaluate_expression("$(QUERY_STRING{nonexistent})", &mut ctx)?;
assert_eq!(result, Value::Null);
Ok(())
}
#[test]
fn test_eval_get_request_query_duplicate_params() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_request(Request::new(
Method::GET,
"http://localhost?x=1&x=2&x=3&y=single",
));
let result = evaluate_expression("$(QUERY_STRING{x})", &mut ctx)?;
match result {
Value::List(items) => {
let items = items.borrow();
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Text("1".into()));
assert_eq!(items[1], Value::Text("2".into()));
assert_eq!(items[2], Value::Text("3".into()));
}
other => panic!("Expected List, got {:?}", other),
}
let result = evaluate_expression("$(QUERY_STRING{y})", &mut ctx)?;
assert_eq!(result, Value::Text("single".into()));
let result = evaluate_expression("$(QUERY_STRING)", &mut ctx)?;
let stringified = result.to_string();
assert!(stringified.contains("&"));
assert!(stringified == "x=1,2,3&y=single" || stringified == "y=single&x=1,2,3");
match result {
Value::Dict(map) => {
let map = map.borrow();
assert_eq!(map.len(), 2);
match map.get("x") {
Some(Value::List(items)) => {
let items = items.borrow();
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Text("1".into()));
assert_eq!(items[1], Value::Text("2".into()));
assert_eq!(items[2], Value::Text("3".into()));
}
other => panic!("Expected List for 'x', got {:?}", other),
}
assert_eq!(map.get("y"), Some(&Value::Text("single".into())));
}
other => panic!("Expected Dict, got {:?}", other),
}
Ok(())
}
#[test]
fn test_eval_get_remote_addr() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_request(Request::new(Method::GET, "http://localhost?hello"));
let result = evaluate_expression("$(REMOTE_ADDR)", &mut ctx)?;
assert_eq!(result, Value::Text("".into()));
Ok(())
}
#[test]
fn test_eval_get_header() -> Result<()> {
let mut ctx = EvalContext::new();
let mut req = Request::new(Method::GET, URL_LOCALHOST);
req.set_header("host", "hello.com");
req.set_header("foobar", "baz");
ctx.set_request(req);
let result = evaluate_expression("$(HTTP_HOST)", &mut ctx)?;
assert_eq!(result, Value::Text("hello.com".into()));
let result = evaluate_expression("$(HTTP_FOOBAR)", &mut ctx)?;
assert_eq!(result, Value::Text("baz".into()));
Ok(())
}
#[test]
fn test_eval_get_header_field() -> Result<()> {
let mut ctx = EvalContext::new();
let mut req = Request::new(Method::GET, URL_LOCALHOST);
req.set_header("Cookie", "foo=bar; bar=baz");
ctx.set_request(req);
let result = evaluate_expression("$(HTTP_COOKIE{'foo'})", &mut ctx)?;
assert_eq!(result, Value::Text("bar".into()));
let result = evaluate_expression("$(HTTP_COOKIE{'bar'})", &mut ctx)?;
assert_eq!(result, Value::Text("baz".into()));
let result = evaluate_expression("$(HTTP_COOKIE{'baz'})", &mut ctx)?;
assert_eq!(result, Value::Null);
Ok(())
}
#[test]
fn test_eval_get_header_as_dict() -> Result<()> {
let mut ctx = EvalContext::new();
let mut req = Request::new(Method::GET, URL_LOCALHOST);
req.set_header("Cookie", "id=571; visits=42");
ctx.set_request(req);
let result = evaluate_expression("$(HTTP_COOKIE)", &mut ctx)?;
assert_eq!(result, Value::Text("id=571; visits=42".into()));
let result = evaluate_expression("$(HTTP_COOKIE{'visits'})", &mut ctx)?;
assert_eq!(result, Value::Text("42".into()));
let result = evaluate_expression("$(HTTP_COOKIE{'id'})", &mut ctx)?;
assert_eq!(result, Value::Text("571".into()));
let result = evaluate_expression("$(HTTP_COOKIE{'nonexistent'})", &mut ctx)?;
assert_eq!(result, Value::Null);
let mut req2 = Request::new(Method::GET, URL_LOCALHOST);
req2.set_header("host", "example.com");
ctx.set_request(req2);
let result = evaluate_expression("$(HTTP_HOST)", &mut ctx)?;
assert_eq!(result, Value::Text("example.com".into()));
Ok(())
}
#[test]
fn test_string_as_list_character_access() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_variable("a_string", None, Value::Text("abcde".into()))?;
let result = evaluate_expression("$(a_string{0})", &mut ctx)?;
assert_eq!(result, Value::Text("a".into()));
let result = evaluate_expression("$(a_string{3})", &mut ctx)?;
assert_eq!(result, Value::Text("d".into()));
let result = evaluate_expression("$(a_string{4})", &mut ctx)?;
assert_eq!(result, Value::Text("e".into()));
let result = evaluate_expression("$(a_string{10})", &mut ctx)?;
assert_eq!(result, Value::Null);
Ok(())
}
#[test]
fn test_logical_operators_with_parentheses() {
let mut ctx = EvalContext::new();
let result = evaluate_expression("(1==1)|('abc'=='def')", &mut ctx).unwrap();
assert_eq!(result.to_string(), "true");
let result = evaluate_expression("(4!=5)&(4==5)", &mut ctx).unwrap();
assert_eq!(result.to_string(), "false");
}
#[test]
fn test_negation_operations() -> Result<()> {
let mut ctx = EvalContext::new();
assert_eq!(
evaluate_expression("!(1 == 2)", &mut ctx)?,
Value::Boolean(true)
);
assert_eq!(
evaluate_expression("!(1 == 1)", &mut ctx)?,
Value::Boolean(false)
);
assert_eq!(
evaluate_expression("!('a' <= 'c')", &mut ctx)?,
Value::Boolean(false)
);
assert_eq!(
evaluate_expression("!!(1 == 1)", &mut ctx)?,
Value::Boolean(true)
);
assert_eq!(
evaluate_expression("!((1==1)&(2==2))", &mut ctx)?,
Value::Boolean(false)
);
assert_eq!(
evaluate_expression("(!(1==1))|(!(2!=2))", &mut ctx)?,
Value::Boolean(true)
);
Ok(())
}
#[test]
fn test_bool_coercion() -> Result<()> {
assert!(Value::Boolean(true).to_bool());
assert!(!Value::Boolean(false).to_bool());
assert!(Value::Integer(1).to_bool());
assert!(!Value::Integer(0).to_bool());
assert!(!Value::Text("".into()).to_bool());
assert!(Value::Text("hello".into()).to_bool());
assert!(!Value::Null.to_bool());
Ok(())
}
#[test]
fn test_numeric_vs_lexicographic_comparison() -> Result<()> {
let result = evaluate_expression("5 > 3", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("10 == 10", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("'5' > '3'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("'10' < '9'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
let mut ctx = EvalContext::new();
ctx.set_variable("numVar", None, Value::Integer(10))
.unwrap();
let result = evaluate_expression("$(numVar) > '9'", &mut ctx)?;
assert_eq!(result, Value::Boolean(false));
let result = evaluate_expression("'10' == '10'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
let mut ctx = EvalContext::new();
ctx.set_variable("version", None, Value::Text("3.01.23".into()))
.unwrap();
let result = evaluate_expression("$(version) == '3.01.23'", &mut ctx)?;
assert_eq!(result, Value::Boolean(true));
ctx.set_variable("version", None, Value::Text("3.01.23".into()))
.unwrap();
let result = evaluate_expression("$(version) < '3.2'", &mut ctx)?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("'2.0' < '10.0'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(false));
Ok(())
}
#[test]
fn test_empty_null_undefined_evaluate_to_false() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.set_variable("empty", None, Value::Text("".into()))
.unwrap();
let result = evaluate_expression("$(empty)", &mut ctx)?;
assert_eq!(result.to_bool(), false);
let result = evaluate_expression("$(nonexistent)", &mut EvalContext::new())?;
assert_eq!(result, Value::Null);
assert_eq!(result.to_bool(), false);
let result = evaluate_expression("'' & 'something'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(false));
let result = evaluate_expression("'' | 'something'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
let result = evaluate_expression("0", &mut EvalContext::new())?;
assert_eq!(result.to_bool(), false);
let result = evaluate_expression("1", &mut EvalContext::new())?;
assert_eq!(result.to_bool(), true);
Ok(())
}
#[test]
fn test_triple_quoted_strings() -> Result<()> {
let result = evaluate_expression("'hello'", &mut EvalContext::new())?;
assert_eq!(result, Value::Text("hello".into()));
let result = evaluate_expression("'''hello'''", &mut EvalContext::new())?;
assert_eq!(result, Value::Text("hello".into()));
let result = evaluate_expression("'''it's working'''", &mut EvalContext::new())?;
assert_eq!(result, Value::Text("it's working".into()));
let result = evaluate_expression("'''test''' == 'test'", &mut EvalContext::new())?;
assert_eq!(result, Value::Boolean(true));
Ok(())
}
#[test]
fn test_string_coercion() -> Result<()> {
assert_eq!(Value::Boolean(true).to_string(), "true");
assert_eq!(Value::Boolean(false).to_string(), "false");
assert_eq!(Value::Integer(1).to_string(), "1");
assert_eq!(Value::Integer(0).to_string(), "0");
assert_eq!(Value::Text("".into()).to_string(), "");
assert_eq!(Value::Text("hello".into()).to_string(), "hello");
assert_eq!(Value::Null.to_string(), "");
Ok(())
}
#[test]
fn test_get_variable_query_string() {
let mut ctx = EvalContext::new();
let req = Request::new(Method::GET, "http://localhost?param=value");
ctx.set_request(req);
let result = ctx.get_variable("QUERY_STRING", None);
match result {
Value::Dict(map) => {
let map = map.borrow();
assert_eq!(map.len(), 1);
assert_eq!(map.get("param"), Some(&Value::Text("value".into())));
}
other => panic!("Expected Dict, got {:?}", other),
}
let result = ctx.get_variable("QUERY_STRING", Some("param"));
assert_eq!(result, Value::Text("value".into()));
let result = ctx.get_variable("QUERY_STRING", Some("nonexistent"));
assert_eq!(result, Value::Null);
}
#[test]
fn test_cache_control_header_uncacheable() {
let mut ctx = EvalContext::new();
ctx.mark_document_uncacheable();
assert_eq!(
ctx.cache_control_header(None),
Some("private, no-cache".to_string())
);
assert_eq!(
ctx.cache_control_header(Some(600)),
Some("private, no-cache".to_string())
);
}
#[test]
fn test_cache_control_header_with_min_ttl() {
let mut ctx = EvalContext::new();
assert_eq!(ctx.cache_control_header(None), None);
ctx.update_cache_min_ttl(300);
assert_eq!(
ctx.cache_control_header(None),
Some("public, max-age=300".to_string())
);
assert_eq!(
ctx.cache_control_header(Some(600)),
Some("public, max-age=600".to_string())
);
ctx.update_cache_min_ttl(600);
ctx.update_cache_min_ttl(200);
assert_eq!(
ctx.cache_control_header(None),
Some("public, max-age=200".to_string())
);
}
#[test]
fn test_range_operator_ascending() -> Result<()> {
let result = evaluate_expression("[1..5]", &mut EvalContext::new())?;
assert_eq!(
result,
Value::new_list(vec![
Value::Integer(1),
Value::Integer(2),
Value::Integer(3),
Value::Integer(4),
Value::Integer(5),
])
);
Ok(())
}
#[test]
fn test_range_operator_descending() -> Result<()> {
let result = evaluate_expression("[5..1]", &mut EvalContext::new())?;
assert_eq!(
result,
Value::new_list(vec![
Value::Integer(5),
Value::Integer(4),
Value::Integer(3),
Value::Integer(2),
Value::Integer(1),
])
);
Ok(())
}
#[test]
fn test_range_operator_single_element() -> Result<()> {
let result = evaluate_expression("[3..3]", &mut EvalContext::new())?;
assert_eq!(result, Value::new_list(vec![Value::Integer(3)]));
Ok(())
}
#[test]
fn test_range_operator_with_variables() -> Result<()> {
let result = evaluate_expression(
"[$(start)..$(end)]",
&mut EvalContext::from([
("start".to_string(), Value::Integer(1)),
("end".to_string(), Value::Integer(10)),
]),
)?;
assert_eq!(
result,
Value::new_list(vec![
Value::Integer(1),
Value::Integer(2),
Value::Integer(3),
Value::Integer(4),
Value::Integer(5),
Value::Integer(6),
Value::Integer(7),
Value::Integer(8),
Value::Integer(9),
Value::Integer(10),
])
);
Ok(())
}
#[test]
fn test_range_operator_in_expression() -> Result<()> {
let result = evaluate_expression("[1..3]", &mut EvalContext::new())?;
if let Value::List(items) = result {
let items = items.borrow();
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Integer(1));
assert_eq!(items[1], Value::Integer(2));
assert_eq!(items[2], Value::Integer(3));
} else {
panic!("Expected a list");
}
Ok(())
}
#[test]
fn test_range_operator_negative_numbers() -> Result<()> {
let result = evaluate_expression("[-2..2]", &mut EvalContext::new())?;
assert_eq!(
result,
Value::new_list(vec![
Value::Integer(-2),
Value::Integer(-1),
Value::Integer(0),
Value::Integer(1),
Value::Integer(2),
])
);
Ok(())
}
#[test]
fn test_range_operator_requires_integers() {
let result = evaluate_expression("['a'..'z']", &mut EvalContext::new());
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("requires integer operands"));
}
#[test]
fn test_args_variable_no_args() -> Result<()> {
let ctx = &mut EvalContext::new();
let result = ctx.get_variable("ARGS", None);
assert_eq!(result, Value::Null);
Ok(())
}
#[test]
fn test_args_variable_with_args() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.push_args(vec![
Value::Text("hello".into()),
Value::Integer(42),
Value::Text("world".into()),
]);
let result = ctx.get_variable("ARGS", None);
if let Value::List(items) = result {
let items = items.borrow();
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Text("hello".into()));
assert_eq!(items[1], Value::Integer(42));
assert_eq!(items[2], Value::Text("world".into()));
} else {
panic!("Expected a list");
}
let result = ctx.get_variable("ARGS", Some("0"));
assert_eq!(result, Value::Text("hello".into()));
let result = ctx.get_variable("ARGS", Some("1"));
assert_eq!(result, Value::Integer(42));
let result = ctx.get_variable("ARGS", Some("2"));
assert_eq!(result, Value::Text("world".into()));
let result = ctx.get_variable("ARGS", Some("3"));
assert_eq!(result, Value::Null);
let result = ctx.get_variable("ARGS", Some("4"));
assert_eq!(result, Value::Null);
ctx.pop_args();
let result = ctx.get_variable("ARGS", None);
assert_eq!(result, Value::Null);
Ok(())
}
#[test]
fn test_args_variable_nested_calls() -> Result<()> {
let mut ctx = EvalContext::new();
ctx.push_args(vec![Value::Integer(10), Value::Integer(20)]);
let result = ctx.get_variable("ARGS", Some("1"));
assert_eq!(result, Value::Integer(20));
ctx.push_args(vec![
Value::Integer(30),
Value::Integer(40),
Value::Integer(50),
]);
let result = ctx.get_variable("ARGS", Some("0"));
assert_eq!(result, Value::Integer(30));
let result = ctx.get_variable("ARGS", Some("2"));
assert_eq!(result, Value::Integer(50));
ctx.pop_args();
let result = ctx.get_variable("ARGS", Some("0"));
assert_eq!(result, Value::Integer(10));
let result = ctx.get_variable("ARGS", Some("1"));
assert_eq!(result, Value::Integer(20));
Ok(())
}
#[test]
fn test_integer_overflow_add() {
let ctx = &mut EvalContext::default();
let result = evaluate_expression(&format!("{} + 1", i64::MAX), ctx);
assert!(result.is_err(), "i64::MAX + 1 should overflow");
}
#[test]
fn test_integer_overflow_sub() {
let ctx = &mut EvalContext::default();
let result = evaluate_expression(&format!("{} - 1", i64::MIN), ctx);
assert!(result.is_err(), "i64::MIN - 1 should overflow");
}
#[test]
fn test_integer_overflow_mul() {
let ctx = &mut EvalContext::default();
let result = evaluate_expression(&format!("{} * 2", i64::MAX), ctx);
assert!(result.is_err(), "i64::MAX * 2 should overflow");
}
#[test]
fn test_integer_no_overflow() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("100 + 200", ctx)?;
assert_eq!(result, Value::Integer(300));
let result = evaluate_expression("100 - 200", ctx)?;
assert_eq!(result, Value::Integer(-100));
let result = evaluate_expression("100 * 200", ctx)?;
assert_eq!(result, Value::Integer(20000));
Ok(())
}
#[test]
fn test_short_circuit_and_false() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("0 & 1", ctx)?;
assert_eq!(result, Value::Boolean(false));
Ok(())
}
#[test]
fn test_short_circuit_or_true() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("1 | 0", ctx)?;
assert_eq!(result, Value::Boolean(true));
Ok(())
}
#[test]
fn test_and_both_true() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("1 & 1", ctx)?;
assert_eq!(result, Value::Boolean(true));
Ok(())
}
#[test]
fn test_or_both_false() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("0 | 0", ctx)?;
assert_eq!(result, Value::Boolean(false));
Ok(())
}
#[test]
fn test_list_concatenation() -> Result<()> {
let ctx = &mut EvalContext::from([
(
"a".to_string(),
Value::new_list(vec![Value::Integer(1), Value::Integer(2)]),
),
(
"b".to_string(),
Value::new_list(vec![Value::Integer(3), Value::Integer(4)]),
),
]);
let result = evaluate_expression("$(a) + $(b)", ctx)?;
if let Value::List(items) = result {
let items = items.borrow();
assert_eq!(items.len(), 4);
assert_eq!(items[0], Value::Integer(1));
assert_eq!(items[1], Value::Integer(2));
assert_eq!(items[2], Value::Integer(3));
assert_eq!(items[3], Value::Integer(4));
} else {
panic!("Expected list, got {result:?}");
}
Ok(())
}
#[test]
fn test_list_concat_does_not_alias() -> Result<()> {
let ctx = &mut EvalContext::from([
("a".to_string(), Value::new_list(vec![Value::Integer(1)])),
("b".to_string(), Value::new_list(vec![Value::Integer(2)])),
]);
let result = evaluate_expression("$(a) + $(b)", ctx)?;
if let Value::List(items) = &result {
assert_eq!(items.borrow().len(), 2);
} else {
panic!("Expected list");
}
let a = ctx.get_variable("a", None);
if let Value::List(items) = a {
assert_eq!(items.borrow().len(), 1);
} else {
panic!("Expected list for a");
}
Ok(())
}
#[test]
fn test_string_repetition() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("3 * 'ab'", ctx)?;
assert_eq!(result, Value::Text(Bytes::from("ababab")));
Ok(())
}
#[test]
fn test_string_repetition_reversed() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("'ab' * 3", ctx)?;
assert_eq!(result, Value::Text(Bytes::from("ababab")));
Ok(())
}
#[test]
fn test_string_repetition_zero() -> Result<()> {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("0 * 'hello'", ctx)?;
assert_eq!(result, Value::Text(Bytes::from("")));
Ok(())
}
#[test]
fn test_string_repetition_negative() {
let ctx = &mut EvalContext::default();
let result = evaluate_expression("-1 * 'hello'", ctx);
assert!(result.is_err(), "Negative repetition should error");
}
#[test]
fn test_list_repetition() -> Result<()> {
let ctx = &mut EvalContext::from([(
"a".to_string(),
Value::new_list(vec![Value::Integer(1), Value::Integer(2)]),
)]);
let result = evaluate_expression("3 * $(a)", ctx)?;
if let Value::List(items) = result {
let items = items.borrow();
assert_eq!(items.len(), 6);
assert_eq!(items[0], Value::Integer(1));
assert_eq!(items[1], Value::Integer(2));
assert_eq!(items[2], Value::Integer(1));
assert_eq!(items[3], Value::Integer(2));
assert_eq!(items[4], Value::Integer(1));
assert_eq!(items[5], Value::Integer(2));
} else {
panic!("Expected list, got {result:?}");
}
Ok(())
}
#[test]
fn test_list_repetition_zero() -> Result<()> {
let ctx =
&mut EvalContext::from([("a".to_string(), Value::new_list(vec![Value::Integer(1)]))]);
let result = evaluate_expression("0 * $(a)", ctx)?;
if let Value::List(items) = result {
assert_eq!(items.borrow().len(), 0);
} else {
panic!("Expected empty list");
}
Ok(())
}
}