Settings hot reloading - Part 1
This commit is contained in:
646
crates/common/src/expr/eval.rs
Normal file
646
crates/common/src/expr/eval.rs
Normal file
@@ -0,0 +1,646 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, fmt::Display};
|
||||
|
||||
use crate::Core;
|
||||
|
||||
use super::{
|
||||
functions::{ResolveVariable, FUNCTIONS},
|
||||
if_block::IfBlock,
|
||||
BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator, Variable,
|
||||
};
|
||||
|
||||
impl Core {
|
||||
pub async fn eval_if<R: for<'x> TryFrom<Variable<'x>>, V: for<'x> ResolveVariable<'x>>(
|
||||
&self,
|
||||
if_block: &IfBlock,
|
||||
resolver: &V,
|
||||
) -> Option<R> {
|
||||
if if_block.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let result = if_block.eval(resolver, self, &if_block.key).await;
|
||||
|
||||
tracing::trace!(context = "eval_if",
|
||||
property = if_block.key,
|
||||
result = ?result,
|
||||
);
|
||||
|
||||
match result.try_into() {
|
||||
Ok(value) => Some(value),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn eval_expr<R: for<'x> TryFrom<Variable<'x>>, V: for<'x> ResolveVariable<'x>>(
|
||||
&self,
|
||||
expr: &Expression,
|
||||
resolver: &V,
|
||||
expr_id: &str,
|
||||
) -> Option<R> {
|
||||
if expr.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let result = expr.eval(resolver, self, expr_id, &mut Vec::new()).await;
|
||||
|
||||
tracing::trace!(context = "eval_expr",
|
||||
property = expr_id,
|
||||
result = ?result,
|
||||
);
|
||||
|
||||
match result.try_into() {
|
||||
Ok(value) => Some(value),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IfBlock {
|
||||
pub async fn eval<'x, V>(&'x self, resolver: &V, core: &Core, property: &str) -> Variable<'x>
|
||||
where
|
||||
V: ResolveVariable<'x>,
|
||||
{
|
||||
let mut captures = Vec::new();
|
||||
|
||||
for if_then in &self.if_then {
|
||||
if if_then
|
||||
.expr
|
||||
.eval(resolver, core, property, &mut captures)
|
||||
.await
|
||||
.to_bool()
|
||||
{
|
||||
return if_then
|
||||
.then
|
||||
.eval(resolver, core, property, &mut captures)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
self.default
|
||||
.eval(resolver, core, property, &mut captures)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
async fn eval<'x, 'y, V>(
|
||||
&'x self,
|
||||
resolver: &V,
|
||||
core: &Core,
|
||||
property: &str,
|
||||
captures: &'y mut Vec<String>,
|
||||
) -> Variable<'x>
|
||||
where
|
||||
V: ResolveVariable<'x>,
|
||||
{
|
||||
let mut stack = Vec::new();
|
||||
let mut exprs = self.items.iter();
|
||||
|
||||
while let Some(expr) = exprs.next() {
|
||||
match expr {
|
||||
ExpressionItem::Variable(v) => {
|
||||
stack.push(resolver.resolve_variable(*v));
|
||||
}
|
||||
ExpressionItem::Constant(val) => {
|
||||
stack.push(Variable::from(val));
|
||||
}
|
||||
ExpressionItem::Capture(v) => {
|
||||
stack.push(Variable::String(Cow::Owned(
|
||||
captures
|
||||
.get(*v as usize)
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
)));
|
||||
}
|
||||
ExpressionItem::UnaryOperator(op) => {
|
||||
let value = stack.pop().unwrap_or_default();
|
||||
stack.push(match op {
|
||||
UnaryOperator::Not => value.op_not(),
|
||||
UnaryOperator::Minus => value.op_minus(),
|
||||
});
|
||||
}
|
||||
ExpressionItem::BinaryOperator(op) => {
|
||||
let right = stack.pop().unwrap_or_default();
|
||||
let left = stack.pop().unwrap_or_default();
|
||||
stack.push(match op {
|
||||
BinaryOperator::Add => left.op_add(right),
|
||||
BinaryOperator::Subtract => left.op_subtract(right),
|
||||
BinaryOperator::Multiply => left.op_multiply(right),
|
||||
BinaryOperator::Divide => left.op_divide(right),
|
||||
BinaryOperator::And => left.op_and(right),
|
||||
BinaryOperator::Or => left.op_or(right),
|
||||
BinaryOperator::Xor => left.op_xor(right),
|
||||
BinaryOperator::Eq => left.op_eq(right),
|
||||
BinaryOperator::Ne => left.op_ne(right),
|
||||
BinaryOperator::Lt => left.op_lt(right),
|
||||
BinaryOperator::Le => left.op_le(right),
|
||||
BinaryOperator::Gt => left.op_gt(right),
|
||||
BinaryOperator::Ge => left.op_ge(right),
|
||||
});
|
||||
}
|
||||
ExpressionItem::Function { id, num_args } => {
|
||||
let num_args = *num_args as usize;
|
||||
|
||||
let mut arguments = Variable::array(num_args);
|
||||
for arg_num in 0..num_args {
|
||||
arguments[num_args - arg_num - 1] = stack.pop().unwrap_or_default();
|
||||
}
|
||||
|
||||
let result = if let Some((_, fnc, _)) = FUNCTIONS.get(*id as usize) {
|
||||
(fnc)(arguments)
|
||||
} else {
|
||||
core.eval_fnc(*id - FUNCTIONS.len() as u32, arguments, property)
|
||||
.await
|
||||
};
|
||||
|
||||
stack.push(result);
|
||||
}
|
||||
ExpressionItem::JmpIf { val, pos } => {
|
||||
if stack.last().map_or(false, |v| v.to_bool()) == *val {
|
||||
for _ in 0..*pos {
|
||||
exprs.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
ExpressionItem::ArrayAccess => {
|
||||
let index = stack
|
||||
.pop()
|
||||
.unwrap_or_default()
|
||||
.to_usize()
|
||||
.unwrap_or_default();
|
||||
let array = stack.pop().unwrap_or_default().into_array();
|
||||
stack.push(array.into_iter().nth(index).unwrap_or_default());
|
||||
}
|
||||
ExpressionItem::ArrayBuild(num_items) => {
|
||||
let num_items = *num_items as usize;
|
||||
let mut items = Variable::array(num_items);
|
||||
for arg_num in 0..num_items {
|
||||
items[num_items - arg_num - 1] = stack.pop().unwrap_or_default();
|
||||
}
|
||||
stack.push(Variable::Array(items));
|
||||
}
|
||||
ExpressionItem::Regex(regex) => {
|
||||
captures.clear();
|
||||
let value = stack.pop().unwrap_or_default().into_string();
|
||||
|
||||
if let Some(captures_) = regex.captures(value.as_ref()) {
|
||||
for capture in captures_.iter() {
|
||||
captures.push(capture.map_or("", |m| m.as_str()).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
stack.push(Variable::Integer(!captures.is_empty() as i64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stack.pop().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.items.is_empty()
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &[ExpressionItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> Variable<'x> {
|
||||
pub fn op_add(self, other: Variable<'x>) -> Variable<'x> {
|
||||
match (self, other) {
|
||||
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_add(b)),
|
||||
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a + b),
|
||||
(Variable::Integer(i), Variable::Float(f))
|
||||
| (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 + f),
|
||||
(Variable::Array(a), Variable::Array(b)) => {
|
||||
Variable::Array(a.into_iter().chain(b).collect::<Vec<_>>())
|
||||
}
|
||||
(Variable::Array(a), b) => {
|
||||
Variable::Array(a.into_iter().chain([b]).collect::<Vec<_>>())
|
||||
}
|
||||
(a, Variable::Array(b)) => {
|
||||
Variable::Array([a].into_iter().chain(b).collect::<Vec<_>>())
|
||||
}
|
||||
(Variable::String(a), b) => {
|
||||
if !a.is_empty() {
|
||||
Variable::String(format!("{}{}", a, b).into())
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
(a, Variable::String(b)) => {
|
||||
if !b.is_empty() {
|
||||
Variable::String(format!("{}{}", a, b).into())
|
||||
} else {
|
||||
a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn op_subtract(self, other: Variable<'x>) -> Variable<'x> {
|
||||
match (self, other) {
|
||||
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_sub(b)),
|
||||
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a - b),
|
||||
(Variable::Integer(a), Variable::Float(b)) => Variable::Float(a as f64 - b),
|
||||
(Variable::Float(a), Variable::Integer(b)) => Variable::Float(a - b as f64),
|
||||
(Variable::Array(a), b) | (b, Variable::Array(a)) => {
|
||||
Variable::Array(a.into_iter().filter(|v| v != &b).collect::<Vec<_>>())
|
||||
}
|
||||
(a, b) => a.parse_number().op_subtract(b.parse_number()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn op_multiply(self, other: Variable<'x>) -> Variable<'x> {
|
||||
match (self, other) {
|
||||
(Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_mul(b)),
|
||||
(Variable::Float(a), Variable::Float(b)) => Variable::Float(a * b),
|
||||
(Variable::Integer(i), Variable::Float(f))
|
||||
| (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 * f),
|
||||
(a, b) => a.parse_number().op_multiply(b.parse_number()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn op_divide(self, other: Variable<'x>) -> Variable<'x> {
|
||||
match (self, other) {
|
||||
(Variable::Integer(a), Variable::Integer(b)) => {
|
||||
Variable::Float(if b != 0 { a as f64 / b as f64 } else { 0.0 })
|
||||
}
|
||||
(Variable::Float(a), Variable::Float(b)) => {
|
||||
Variable::Float(if b != 0.0 { a / b } else { 0.0 })
|
||||
}
|
||||
(Variable::Integer(a), Variable::Float(b)) => {
|
||||
Variable::Float(if b != 0.0 { a as f64 / b } else { 0.0 })
|
||||
}
|
||||
(Variable::Float(a), Variable::Integer(b)) => {
|
||||
Variable::Float(if b != 0 { a / b as f64 } else { 0.0 })
|
||||
}
|
||||
(a, b) => a.parse_number().op_divide(b.parse_number()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn op_and(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self.to_bool() & other.to_bool()))
|
||||
}
|
||||
|
||||
pub fn op_or(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self.to_bool() | other.to_bool()))
|
||||
}
|
||||
|
||||
pub fn op_xor(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self.to_bool() ^ other.to_bool()))
|
||||
}
|
||||
|
||||
pub fn op_eq(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self == other))
|
||||
}
|
||||
|
||||
pub fn op_ne(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self != other))
|
||||
}
|
||||
|
||||
pub fn op_lt(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self < other))
|
||||
}
|
||||
|
||||
pub fn op_le(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self <= other))
|
||||
}
|
||||
|
||||
pub fn op_gt(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self > other))
|
||||
}
|
||||
|
||||
pub fn op_ge(self, other: Variable) -> Variable {
|
||||
Variable::Integer(i64::from(self >= other))
|
||||
}
|
||||
|
||||
pub fn op_not(self) -> Variable<'static> {
|
||||
Variable::Integer(i64::from(!self.to_bool()))
|
||||
}
|
||||
|
||||
pub fn op_minus(self) -> Variable<'static> {
|
||||
match self {
|
||||
Variable::Integer(n) => Variable::Integer(-n),
|
||||
Variable::Float(n) => Variable::Float(-n),
|
||||
_ => self.parse_number().op_minus(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_number(&self) -> Variable<'static> {
|
||||
match self {
|
||||
Variable::String(s) if !s.is_empty() => {
|
||||
if let Ok(n) = s.parse::<i64>() {
|
||||
Variable::Integer(n)
|
||||
} else if let Ok(n) = s.parse::<f64>() {
|
||||
Variable::Float(n)
|
||||
} else {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
Variable::Integer(n) => Variable::Integer(*n),
|
||||
Variable::Float(n) => Variable::Float(*n),
|
||||
Variable::Array(l) => Variable::Integer(l.is_empty() as i64),
|
||||
_ => Variable::Integer(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn array(num_items: usize) -> Vec<Variable<'static>> {
|
||||
let mut items = Vec::with_capacity(num_items);
|
||||
for _ in 0..num_items {
|
||||
items.push(Variable::Integer(0));
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
pub fn to_ref<'y: 'x>(&'y self) -> Variable<'x> {
|
||||
match self {
|
||||
Variable::String(s) => Variable::String(Cow::Borrowed(s.as_ref())),
|
||||
Variable::Integer(n) => Variable::Integer(*n),
|
||||
Variable::Float(n) => Variable::Float(*n),
|
||||
Variable::Array(l) => Variable::Array(l.iter().map(|v| v.to_ref()).collect::<Vec<_>>()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bool(&self) -> bool {
|
||||
match self {
|
||||
Variable::Float(f) => *f != 0.0,
|
||||
Variable::Integer(n) => *n != 0,
|
||||
Variable::String(s) => !s.is_empty(),
|
||||
Variable::Array(a) => !a.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
Variable::String(s) => Cow::Borrowed(s.as_ref()),
|
||||
Variable::Integer(n) => Cow::Owned(n.to_string()),
|
||||
Variable::Float(n) => Cow::Owned(n.to_string()),
|
||||
Variable::Array(l) => {
|
||||
let mut result = String::with_capacity(self.len() * 10);
|
||||
for item in l {
|
||||
if !result.is_empty() {
|
||||
result.push_str("\r\n");
|
||||
}
|
||||
match item {
|
||||
Variable::String(v) => result.push_str(v),
|
||||
Variable::Integer(v) => result.push_str(&v.to_string()),
|
||||
Variable::Float(v) => result.push_str(&v.to_string()),
|
||||
Variable::Array(_) => {}
|
||||
}
|
||||
}
|
||||
Cow::Owned(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> Cow<'x, str> {
|
||||
match self {
|
||||
Variable::String(s) => s,
|
||||
Variable::Integer(n) => Cow::Owned(n.to_string()),
|
||||
Variable::Float(n) => Cow::Owned(n.to_string()),
|
||||
Variable::Array(l) => {
|
||||
let mut result = String::with_capacity(l.len() * 10);
|
||||
for item in l {
|
||||
if !result.is_empty() {
|
||||
result.push_str("\r\n");
|
||||
}
|
||||
match item {
|
||||
Variable::String(v) => result.push_str(v.as_ref()),
|
||||
Variable::Integer(v) => result.push_str(&v.to_string()),
|
||||
Variable::Float(v) => result.push_str(&v.to_string()),
|
||||
Variable::Array(_) => {}
|
||||
}
|
||||
}
|
||||
Cow::Owned(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_integer(&self) -> Option<i64> {
|
||||
match self {
|
||||
Variable::Integer(n) => Some(*n),
|
||||
Variable::Float(n) => Some(*n as i64),
|
||||
Variable::String(s) if !s.is_empty() => s.parse::<i64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_usize(&self) -> Option<usize> {
|
||||
match self {
|
||||
Variable::Integer(n) => Some(*n as usize),
|
||||
Variable::Float(n) => Some(*n as usize),
|
||||
Variable::String(s) if !s.is_empty() => s.parse::<usize>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
Variable::String(s) => s.len(),
|
||||
Variable::Integer(_) | Variable::Float(_) => 2,
|
||||
Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Variable::String(s) => s.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_array(&self) -> Option<&[Variable]> {
|
||||
match self {
|
||||
Variable::Array(l) => Some(l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_array(self) -> Vec<Variable<'x>> {
|
||||
match self {
|
||||
Variable::Array(l) => l,
|
||||
v if !v.is_empty() => vec![v],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_array(&self) -> Vec<Variable<'_>> {
|
||||
match self {
|
||||
Variable::Array(l) => l.iter().map(|v| v.to_ref()).collect::<Vec<_>>(),
|
||||
v if !v.is_empty() => vec![v.to_ref()],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Variable<'static> {
|
||||
match self {
|
||||
Variable::String(s) => Variable::String(Cow::Owned(s.into_owned())),
|
||||
Variable::Integer(n) => Variable::Integer(n),
|
||||
Variable::Float(n) => Variable::Float(n),
|
||||
Variable::Array(l) => Variable::Array(l.into_iter().map(|v| v.into_owned()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Variable<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Integer(a), Self::Integer(b)) => a == b,
|
||||
(Self::Float(a), Self::Float(b)) => a == b,
|
||||
(Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => {
|
||||
*a as f64 == *b
|
||||
}
|
||||
(Self::String(a), Self::String(b)) => a == b,
|
||||
(Self::String(_), Self::Integer(_) | Self::Float(_)) => &self.parse_number() == other,
|
||||
(Self::Integer(_) | Self::Float(_), Self::String(_)) => self == &other.parse_number(),
|
||||
(Self::Array(a), Self::Array(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Variable<'_> {}
|
||||
|
||||
#[allow(clippy::non_canonical_partial_ord_impl)]
|
||||
impl PartialOrd for Variable<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match (self, other) {
|
||||
(Self::Integer(a), Self::Integer(b)) => a.partial_cmp(b),
|
||||
(Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
|
||||
(Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b),
|
||||
(Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)),
|
||||
(Self::String(a), Self::String(b)) => a.partial_cmp(b),
|
||||
(Self::String(_), Self::Integer(_) | Self::Float(_)) => {
|
||||
self.parse_number().partial_cmp(other)
|
||||
}
|
||||
(Self::Integer(_) | Self::Float(_), Self::String(_)) => {
|
||||
self.partial_cmp(&other.parse_number())
|
||||
}
|
||||
(Self::Array(a), Self::Array(b)) => a.partial_cmp(b),
|
||||
(Self::Array(_) | Self::String(_), _) => Ordering::Greater.into(),
|
||||
(_, Self::Array(_)) => Ordering::Less.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Variable<'_> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap_or(Ordering::Greater)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Variable<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Variable::String(v) => v.fmt(f),
|
||||
Variable::Integer(v) => v.fmt(f),
|
||||
Variable::Float(v) => v.fmt(f),
|
||||
Variable::Array(v) => {
|
||||
for (i, v) in v.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
v.fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoBool {
|
||||
fn into_bool(self) -> bool;
|
||||
}
|
||||
|
||||
impl IntoBool for f64 {
|
||||
#[inline(always)]
|
||||
fn into_bool(self) -> bool {
|
||||
self != 0.0
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoBool for i64 {
|
||||
#[inline(always)]
|
||||
fn into_bool(self) -> bool {
|
||||
self != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x Constant> for Variable<'x> {
|
||||
fn from(value: &'x Constant) -> Self {
|
||||
match value {
|
||||
Constant::Integer(i) => Variable::Integer(*i),
|
||||
Constant::Float(f) => Variable::Float(*f),
|
||||
Constant::String(s) => Variable::String(s.as_str().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for String {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
if let Variable::String(s) = value {
|
||||
Ok(s.into_owned())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<Variable<'x>> for bool {
|
||||
fn from(val: Variable<'x>) -> Self {
|
||||
val.to_bool()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for i64 {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
value.to_integer().ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for u64 {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
value.to_integer().map(|v| v as u64).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for usize {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
value.to_usize().ok_or(())
|
||||
}
|
||||
}
|
||||
82
crates/common/src/expr/functions/array.rs
Normal file
82
crates/common/src/expr/functions/array.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::expr::Variable;
|
||||
|
||||
pub(crate) fn fn_count(v: Vec<Variable>) -> Variable {
|
||||
match &v[0] {
|
||||
Variable::Array(a) => a.len(),
|
||||
v => {
|
||||
if !v.is_empty() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sort(mut v: Vec<Variable>) -> Variable {
|
||||
let is_asc = v[1].to_bool();
|
||||
let mut arr = v.remove(0).into_array();
|
||||
if is_asc {
|
||||
arr.sort_unstable_by(|a, b| b.cmp(a));
|
||||
} else {
|
||||
arr.sort_unstable();
|
||||
}
|
||||
arr.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_dedup(mut v: Vec<Variable>) -> Variable {
|
||||
let arr = v.remove(0).into_array();
|
||||
let mut result = Vec::with_capacity(arr.len());
|
||||
|
||||
for item in arr {
|
||||
if !result.contains(&item) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
result.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_intersect(v: Vec<Variable>) -> Variable {
|
||||
match (&v[0], &v[1]) {
|
||||
(Variable::Array(a), Variable::Array(b)) => a.iter().any(|x| b.contains(x)),
|
||||
(Variable::Array(a), item) | (item, Variable::Array(a)) => a.contains(item),
|
||||
_ => false,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_winnow(mut v: Vec<Variable>) -> Variable {
|
||||
match v.remove(0) {
|
||||
Variable::Array(a) => a
|
||||
.into_iter()
|
||||
.filter(|i| !i.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
v => v,
|
||||
}
|
||||
}
|
||||
394
crates/common/src/expr/functions/asynch.rs
Normal file
394
crates/common/src/expr/functions/asynch.rs
Normal file
@@ -0,0 +1,394 @@
|
||||
use std::{cmp::Ordering, net::IpAddr, vec::IntoIter};
|
||||
|
||||
use mail_auth::IpLookupStrategy;
|
||||
use store::{Deserialize, Rows, Value};
|
||||
|
||||
use crate::Core;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Core {
|
||||
pub(crate) async fn eval_fnc<'x>(
|
||||
&self,
|
||||
fnc_id: u32,
|
||||
params: Vec<Variable<'x>>,
|
||||
property: &str,
|
||||
) -> Variable<'x> {
|
||||
let mut params = FncParams::new(params);
|
||||
|
||||
match fnc_id {
|
||||
F_IS_LOCAL_DOMAIN => {
|
||||
let directory = params.next_as_string();
|
||||
let domain = params.next_as_string();
|
||||
|
||||
self.get_directory_or_default(directory.as_ref())
|
||||
.is_local_domain(domain.as_ref())
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to check if domain is local."
|
||||
);
|
||||
|
||||
false
|
||||
})
|
||||
.into()
|
||||
}
|
||||
F_IS_LOCAL_ADDRESS => {
|
||||
let directory = params.next_as_string();
|
||||
let address = params.next_as_string();
|
||||
|
||||
self.get_directory_or_default(directory.as_ref())
|
||||
.rcpt(address.as_ref())
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to check if address is local."
|
||||
);
|
||||
|
||||
false
|
||||
})
|
||||
.into()
|
||||
}
|
||||
F_KEY_GET => {
|
||||
let store = params.next_as_string();
|
||||
let key = params.next_as_string();
|
||||
|
||||
self.get_lookup_store(store.as_ref())
|
||||
.key_get::<VariableWrapper>(key.into_owned().into_bytes())
|
||||
.await
|
||||
.map(|value| value.map(|v| v.into_inner()).unwrap_or_default())
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to get key."
|
||||
);
|
||||
|
||||
Variable::default()
|
||||
})
|
||||
}
|
||||
F_KEY_EXISTS => {
|
||||
let store = params.next_as_string();
|
||||
let key = params.next_as_string();
|
||||
|
||||
self.get_lookup_store(store.as_ref())
|
||||
.key_exists(key.into_owned().into_bytes())
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to get key."
|
||||
);
|
||||
|
||||
false
|
||||
})
|
||||
.into()
|
||||
}
|
||||
F_KEY_SET => {
|
||||
let store = params.next_as_string();
|
||||
let key = params.next_as_string();
|
||||
let value = params.next_as_string();
|
||||
|
||||
self.get_lookup_store(store.as_ref())
|
||||
.key_set(
|
||||
key.into_owned().into_bytes(),
|
||||
value.into_owned().into_bytes(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to set key."
|
||||
);
|
||||
|
||||
false
|
||||
})
|
||||
.into()
|
||||
}
|
||||
F_COUNTER_INCR => {
|
||||
let store = params.next_as_string();
|
||||
let key = params.next_as_string();
|
||||
let value = params.next_as_integer();
|
||||
|
||||
self.get_lookup_store(store.as_ref())
|
||||
.counter_incr(key.into_owned().into_bytes(), value, None, true)
|
||||
.await
|
||||
.map(Variable::Integer)
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to increment counter."
|
||||
);
|
||||
|
||||
Variable::default()
|
||||
})
|
||||
}
|
||||
F_COUNTER_GET => {
|
||||
let store = params.next_as_string();
|
||||
let key = params.next_as_string();
|
||||
|
||||
self.get_lookup_store(store.as_ref())
|
||||
.counter_get(key.into_owned().into_bytes())
|
||||
.await
|
||||
.map(Variable::Integer)
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(
|
||||
context = "eval_if",
|
||||
event = "error",
|
||||
property = property,
|
||||
error = ?err,
|
||||
"Failed to increment counter."
|
||||
);
|
||||
|
||||
Variable::default()
|
||||
})
|
||||
}
|
||||
F_DNS_QUERY => self.dns_query(params).await,
|
||||
F_SQL_QUERY => self.sql_query(params).await,
|
||||
_ => Variable::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn sql_query<'x>(&self, mut arguments: FncParams<'x>) -> Variable<'x> {
|
||||
let store = self.get_lookup_store(arguments.next_as_string().as_ref());
|
||||
let query = arguments.next_as_string();
|
||||
|
||||
if query.is_empty() {
|
||||
tracing::warn!(
|
||||
context = "eval:sql_query",
|
||||
event = "invalid",
|
||||
reason = "Empty query string",
|
||||
);
|
||||
return Variable::default();
|
||||
}
|
||||
|
||||
// Obtain arguments
|
||||
let arguments = match arguments.next() {
|
||||
Variable::Array(l) => l.into_iter().map(to_store_value).collect(),
|
||||
v => vec![to_store_value(v)],
|
||||
};
|
||||
|
||||
// Run query
|
||||
if query
|
||||
.as_bytes()
|
||||
.get(..6)
|
||||
.map_or(false, |q| q.eq_ignore_ascii_case(b"SELECT"))
|
||||
{
|
||||
if let Ok(mut rows) = store.query::<Rows>(&query, arguments).await {
|
||||
match rows.rows.len().cmp(&1) {
|
||||
Ordering::Equal => {
|
||||
let mut row = rows.rows.pop().unwrap().values;
|
||||
match row.len().cmp(&1) {
|
||||
Ordering::Equal if !matches!(row.first(), Some(Value::Null)) => {
|
||||
row.pop().map(into_variable).unwrap()
|
||||
}
|
||||
Ordering::Less => Variable::default(),
|
||||
_ => Variable::Array(
|
||||
row.into_iter().map(into_variable).collect::<Vec<_>>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
Ordering::Less => Variable::default(),
|
||||
Ordering::Greater => rows
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
Variable::Array(
|
||||
r.values.into_iter().map(into_variable).collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
}
|
||||
} else {
|
||||
false.into()
|
||||
}
|
||||
} else {
|
||||
store.query::<usize>(&query, arguments).await.is_ok().into()
|
||||
}
|
||||
}
|
||||
|
||||
async fn dns_query<'x>(&self, mut arguments: FncParams<'x>) -> Variable<'x> {
|
||||
let entry = arguments.next_as_string();
|
||||
let record_type = arguments.next_as_string();
|
||||
|
||||
if record_type.eq_ignore_ascii_case("ip") {
|
||||
match self
|
||||
.smtp
|
||||
.resolvers
|
||||
.dns
|
||||
.ip_lookup(entry.as_ref(), IpLookupStrategy::Ipv4thenIpv6, 10)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result
|
||||
.iter()
|
||||
.map(|ip| Variable::from(ip.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Err(_) => Variable::default(),
|
||||
}
|
||||
} else if record_type.eq_ignore_ascii_case("mx") {
|
||||
match self.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await {
|
||||
Ok(result) => result
|
||||
.iter()
|
||||
.flat_map(|mx| {
|
||||
mx.exchanges.iter().map(|host| {
|
||||
Variable::String(
|
||||
host.strip_suffix('.')
|
||||
.unwrap_or(host.as_str())
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Err(_) => Variable::default(),
|
||||
}
|
||||
} else if record_type.eq_ignore_ascii_case("txt") {
|
||||
match self.smtp.resolvers.dns.txt_raw_lookup(entry.as_ref()).await {
|
||||
Ok(result) => Variable::from(String::from_utf8(result).unwrap_or_default()),
|
||||
Err(_) => Variable::default(),
|
||||
}
|
||||
} else if record_type.eq_ignore_ascii_case("ptr") {
|
||||
if let Ok(addr) = entry.parse::<IpAddr>() {
|
||||
match self.smtp.resolvers.dns.ptr_lookup(addr).await {
|
||||
Ok(result) => result
|
||||
.iter()
|
||||
.map(|host| Variable::from(host.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Err(_) => Variable::default(),
|
||||
}
|
||||
} else {
|
||||
Variable::default()
|
||||
}
|
||||
} else if record_type.eq_ignore_ascii_case("ipv4") {
|
||||
match self.smtp.resolvers.dns.ipv4_lookup(entry.as_ref()).await {
|
||||
Ok(result) => result
|
||||
.iter()
|
||||
.map(|ip| Variable::from(ip.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Err(_) => Variable::default(),
|
||||
}
|
||||
} else if record_type.eq_ignore_ascii_case("ipv6") {
|
||||
match self.smtp.resolvers.dns.ipv6_lookup(entry.as_ref()).await {
|
||||
Ok(result) => result
|
||||
.iter()
|
||||
.map(|ip| Variable::from(ip.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Err(_) => Variable::default(),
|
||||
}
|
||||
} else {
|
||||
Variable::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FncParams<'x> {
|
||||
params: IntoIter<Variable<'x>>,
|
||||
}
|
||||
|
||||
impl<'x> FncParams<'x> {
|
||||
pub fn new(params: Vec<Variable<'x>>) -> Self {
|
||||
Self {
|
||||
params: params.into_iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_as_string(&mut self) -> Cow<'x, str> {
|
||||
self.params.next().unwrap().into_string()
|
||||
}
|
||||
|
||||
pub fn next_as_integer(&mut self) -> i64 {
|
||||
self.params.next().unwrap().to_integer().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Variable<'x> {
|
||||
self.params.next().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct VariableWrapper(Variable<'static>);
|
||||
|
||||
impl From<i64> for VariableWrapper {
|
||||
fn from(value: i64) -> Self {
|
||||
VariableWrapper(Variable::Integer(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for VariableWrapper {
|
||||
fn deserialize(bytes: &[u8]) -> store::Result<Self> {
|
||||
String::deserialize(bytes).map(|v| VariableWrapper(Variable::String(v.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<store::Value<'static>> for VariableWrapper {
|
||||
fn from(value: store::Value<'static>) -> Self {
|
||||
VariableWrapper(match value {
|
||||
Value::Integer(v) => Variable::Integer(v),
|
||||
Value::Bool(v) => Variable::Integer(v as i64),
|
||||
Value::Float(v) => Variable::Float(v),
|
||||
Value::Text(v) => Variable::String(v),
|
||||
Value::Blob(v) => Variable::String(match v {
|
||||
std::borrow::Cow::Borrowed(v) => String::from_utf8_lossy(v),
|
||||
std::borrow::Cow::Owned(v) => String::from_utf8_lossy(&v).into_owned().into(),
|
||||
}),
|
||||
Value::Null => Variable::String("".into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VariableWrapper {
|
||||
pub fn into_inner(self) -> Variable<'static> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn to_store_value(value: Variable) -> Value {
|
||||
match value {
|
||||
Variable::String(v) => Value::Text(v),
|
||||
Variable::Integer(v) => Value::Integer(v),
|
||||
Variable::Float(v) => Value::Float(v),
|
||||
v => Value::Text(v.to_string().into_owned().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_variable(value: Value) -> Variable {
|
||||
match value {
|
||||
Value::Integer(v) => Variable::Integer(v),
|
||||
Value::Bool(v) => Variable::Integer(i64::from(v)),
|
||||
Value::Float(v) => Variable::Float(v),
|
||||
Value::Text(v) => Variable::String(v),
|
||||
Value::Blob(v) => Variable::String(
|
||||
String::from_utf8(v.into_owned())
|
||||
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
|
||||
.into(),
|
||||
),
|
||||
Value::Null => Variable::default(),
|
||||
}
|
||||
}
|
||||
121
crates/common/src/expr/functions/email.rs
Normal file
121
crates/common/src/expr/functions/email.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::expr::Variable;
|
||||
|
||||
pub(crate) fn fn_is_email(v: Vec<Variable>) -> Variable {
|
||||
let mut last_ch = 0;
|
||||
let mut in_quote = false;
|
||||
let mut at_count = 0;
|
||||
let mut dot_count = 0;
|
||||
let mut lp_len = 0;
|
||||
let mut value = 0;
|
||||
|
||||
for ch in v[0].to_string().bytes() {
|
||||
match ch {
|
||||
b'0'..=b'9'
|
||||
| b'a'..=b'z'
|
||||
| b'A'..=b'Z'
|
||||
| b'!'
|
||||
| b'#'
|
||||
| b'$'
|
||||
| b'%'
|
||||
| b'&'
|
||||
| b'\''
|
||||
| b'*'
|
||||
| b'+'
|
||||
| b'-'
|
||||
| b'/'
|
||||
| b'='
|
||||
| b'?'
|
||||
| b'^'
|
||||
| b'_'
|
||||
| b'`'
|
||||
| b'{'
|
||||
| b'|'
|
||||
| b'}'
|
||||
| b'~'
|
||||
| 0x7f..=u8::MAX => {
|
||||
value += 1;
|
||||
}
|
||||
b'.' if !in_quote => {
|
||||
if last_ch != b'.' && last_ch != b'@' && value != 0 {
|
||||
value += 1;
|
||||
if at_count == 1 {
|
||||
dot_count += 1;
|
||||
}
|
||||
} else {
|
||||
return false.into();
|
||||
}
|
||||
}
|
||||
b'@' if !in_quote => {
|
||||
at_count += 1;
|
||||
lp_len = value;
|
||||
value = 0;
|
||||
}
|
||||
b'>' | b':' | b',' | b' ' if in_quote => {
|
||||
value += 1;
|
||||
}
|
||||
b'\"' if !in_quote || last_ch != b'\\' => {
|
||||
in_quote = !in_quote;
|
||||
}
|
||||
b'\\' if in_quote && last_ch != b'\\' => (),
|
||||
_ => {
|
||||
if !in_quote {
|
||||
return false.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
(at_count == 1 && dot_count > 0 && lp_len > 0 && value > 0).into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_email_part(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap();
|
||||
let part = v.next().unwrap().into_string();
|
||||
|
||||
value.transform(|s| match s {
|
||||
Cow::Borrowed(s) => s
|
||||
.rsplit_once('@')
|
||||
.map(|(u, d)| match part.as_ref() {
|
||||
"local" => Variable::from(u.trim()),
|
||||
"domain" => Variable::from(d.trim()),
|
||||
_ => Variable::default(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
Cow::Owned(s) => s
|
||||
.rsplit_once('@')
|
||||
.map(|(u, d)| match part.as_ref() {
|
||||
"local" => Variable::from(u.trim().to_string()),
|
||||
"domain" => Variable::from(d.trim().to_string()),
|
||||
_ => Variable::default(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
67
crates/common/src/expr/functions/misc.rs
Normal file
67
crates/common/src/expr/functions/misc.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use mail_auth::common::resolver::ToReverseName;
|
||||
|
||||
use crate::expr::Variable;
|
||||
|
||||
pub(crate) fn fn_is_empty(v: Vec<Variable>) -> Variable {
|
||||
match &v[0] {
|
||||
Variable::String(s) => s.is_empty(),
|
||||
Variable::Integer(_) | Variable::Float(_) => false,
|
||||
Variable::Array(a) => a.is_empty(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_number(v: Vec<Variable>) -> Variable {
|
||||
matches!(&v[0], Variable::Integer(_) | Variable::Float(_)).into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_ip_addr(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string().parse::<std::net::IpAddr>().is_ok().into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_ipv4_addr(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.parse::<std::net::IpAddr>()
|
||||
.map_or(false, |ip| matches!(ip, IpAddr::V4(_)))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_ipv6_addr(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.parse::<std::net::IpAddr>()
|
||||
.map_or(false, |ip| matches!(ip, IpAddr::V6(_)))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_ip_reverse_name(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.parse::<std::net::IpAddr>()
|
||||
.map(|ip| ip.to_reverse_name())
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
}
|
||||
119
crates/common/src/expr/functions/mod.rs
Normal file
119
crates/common/src/expr/functions/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::Variable;
|
||||
|
||||
pub mod array;
|
||||
pub mod asynch;
|
||||
pub mod email;
|
||||
pub mod misc;
|
||||
pub mod text;
|
||||
|
||||
pub trait ResolveVariable<'x> {
|
||||
fn resolve_variable(&self, variable: u32) -> Variable<'x>;
|
||||
}
|
||||
|
||||
impl<'x> Variable<'x> {
|
||||
fn transform(self, f: impl Fn(Cow<'x, str>) -> Variable<'x>) -> Variable<'x> {
|
||||
match self {
|
||||
Variable::String(s) => f(s),
|
||||
Variable::Array(list) => Variable::Array(
|
||||
list.into_iter()
|
||||
.map(|v| match v {
|
||||
Variable::String(s) => f(s),
|
||||
v => f(v.into_string()),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
v => f(v.into_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) const FUNCTIONS: &[(&str, fn(Vec<Variable>) -> Variable, u32)] = &[
|
||||
("count", array::fn_count, 1),
|
||||
("sort", array::fn_sort, 2),
|
||||
("dedup", array::fn_dedup, 1),
|
||||
("winnow", array::fn_winnow, 1),
|
||||
("is_intersect", array::fn_is_intersect, 2),
|
||||
("is_email", email::fn_is_email, 1),
|
||||
("email_part", email::fn_email_part, 2),
|
||||
("is_empty", misc::fn_is_empty, 1),
|
||||
("is_number", misc::fn_is_number, 1),
|
||||
("is_ip_addr", misc::fn_is_ip_addr, 1),
|
||||
("is_ipv4_addr", misc::fn_is_ipv4_addr, 1),
|
||||
("is_ipv6_addr", misc::fn_is_ipv6_addr, 1),
|
||||
("ip_reverse_name", misc::fn_ip_reverse_name, 1),
|
||||
("trim", text::fn_trim, 1),
|
||||
("trim_end", text::fn_trim_end, 1),
|
||||
("trim_start", text::fn_trim_start, 1),
|
||||
("len", text::fn_len, 1),
|
||||
("to_lowercase", text::fn_to_lowercase, 1),
|
||||
("to_uppercase", text::fn_to_uppercase, 1),
|
||||
("is_uppercase", text::fn_is_uppercase, 1),
|
||||
("is_lowercase", text::fn_is_lowercase, 1),
|
||||
("has_digits", text::fn_has_digits, 1),
|
||||
("count_spaces", text::fn_count_spaces, 1),
|
||||
("count_uppercase", text::fn_count_uppercase, 1),
|
||||
("count_lowercase", text::fn_count_lowercase, 1),
|
||||
("count_chars", text::fn_count_chars, 1),
|
||||
("contains", text::fn_contains, 2),
|
||||
("contains_ignore_case", text::fn_contains_ignore_case, 2),
|
||||
("eq_ignore_case", text::fn_eq_ignore_case, 2),
|
||||
("starts_with", text::fn_starts_with, 2),
|
||||
("ends_with", text::fn_ends_with, 2),
|
||||
("lines", text::fn_lines, 1),
|
||||
("substring", text::fn_substring, 3),
|
||||
("strip_prefix", text::fn_strip_prefix, 2),
|
||||
("strip_suffix", text::fn_strip_suffix, 2),
|
||||
("split", text::fn_split, 2),
|
||||
("rsplit", text::fn_rsplit, 2),
|
||||
("split_once", text::fn_split_once, 2),
|
||||
("rsplit_once", text::fn_rsplit_once, 2),
|
||||
("split_words", text::fn_split_words, 1),
|
||||
];
|
||||
|
||||
pub const F_IS_LOCAL_DOMAIN: u32 = 0;
|
||||
pub const F_IS_LOCAL_ADDRESS: u32 = 1;
|
||||
pub const F_KEY_GET: u32 = 2;
|
||||
pub const F_KEY_EXISTS: u32 = 3;
|
||||
pub const F_KEY_SET: u32 = 4;
|
||||
pub const F_COUNTER_INCR: u32 = 5;
|
||||
pub const F_COUNTER_GET: u32 = 6;
|
||||
pub const F_SQL_QUERY: u32 = 7;
|
||||
pub const F_DNS_QUERY: u32 = 8;
|
||||
|
||||
pub const ASYNC_FUNCTIONS: &[(&str, u32, u32)] = &[
|
||||
("is_local_domain", F_IS_LOCAL_DOMAIN, 2),
|
||||
("is_local_address", F_IS_LOCAL_ADDRESS, 2),
|
||||
("key_get", F_KEY_GET, 2),
|
||||
("key_exists", F_KEY_EXISTS, 2),
|
||||
("key_set", F_KEY_SET, 3),
|
||||
("counter_incr", F_COUNTER_INCR, 3),
|
||||
("counter_get", F_COUNTER_GET, 2),
|
||||
("dns_query", F_DNS_QUERY, 2),
|
||||
("sql_query", F_SQL_QUERY, 3),
|
||||
];
|
||||
301
crates/common/src/expr/functions/text.rs
Normal file
301
crates/common/src/expr/functions/text.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::expr::Variable;
|
||||
|
||||
pub(crate) fn fn_trim(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| match s {
|
||||
Cow::Borrowed(s) => Variable::from(s.trim()),
|
||||
Cow::Owned(s) => Variable::from(s.trim().to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_trim_end(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| match s {
|
||||
Cow::Borrowed(s) => Variable::from(s.trim_end()),
|
||||
Cow::Owned(s) => Variable::from(s.trim_end().to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_trim_start(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| match s {
|
||||
Cow::Borrowed(s) => Variable::from(s.trim_start()),
|
||||
Cow::Owned(s) => Variable::from(s.trim_start().to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_len(v: Vec<Variable>) -> Variable {
|
||||
match &v[0] {
|
||||
Variable::String(s) => s.len(),
|
||||
Variable::Array(a) => a.len(),
|
||||
v => v.to_string().len(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_to_lowercase(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| Variable::from(s.to_lowercase()))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_to_uppercase(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| Variable::from(s.to_uppercase()))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_uppercase(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| {
|
||||
s.chars()
|
||||
.filter(|c| c.is_alphabetic())
|
||||
.all(|c| c.is_uppercase())
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_is_lowercase(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0).transform(|s| {
|
||||
s.chars()
|
||||
.filter(|c| c.is_alphabetic())
|
||||
.all(|c| c.is_lowercase())
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_has_digits(mut v: Vec<Variable>) -> Variable {
|
||||
v.remove(0)
|
||||
.transform(|s| s.chars().any(|c| c.is_ascii_digit()).into())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_split_words(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.split_whitespace()
|
||||
.filter(|word| word.chars().all(|c| c.is_alphanumeric()))
|
||||
.map(|word| Variable::from(word.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_count_spaces(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.as_ref()
|
||||
.chars()
|
||||
.filter(|c| c.is_whitespace())
|
||||
.count()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_count_uppercase(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.as_ref()
|
||||
.chars()
|
||||
.filter(|c| c.is_alphabetic() && c.is_uppercase())
|
||||
.count()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_count_lowercase(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.as_ref()
|
||||
.chars()
|
||||
.filter(|c| c.is_alphabetic() && c.is_lowercase())
|
||||
.count()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_count_chars(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string().as_ref().chars().count().into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_eq_ignore_case(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.eq_ignore_ascii_case(v[1].to_string().as_ref())
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_contains(v: Vec<Variable>) -> Variable {
|
||||
match &v[0] {
|
||||
Variable::String(s) => s.contains(v[1].to_string().as_ref()),
|
||||
Variable::Array(arr) => arr.contains(&v[1]),
|
||||
val => val.to_string().contains(v[1].to_string().as_ref()),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_contains_ignore_case(v: Vec<Variable>) -> Variable {
|
||||
let needle = v[1].to_string();
|
||||
match &v[0] {
|
||||
Variable::String(s) => s.to_lowercase().contains(&needle.to_lowercase()),
|
||||
Variable::Array(arr) => arr.iter().any(|v| match v {
|
||||
Variable::String(s) => s.eq_ignore_ascii_case(needle.as_ref()),
|
||||
_ => false,
|
||||
}),
|
||||
val => val.to_string().contains(needle.as_ref()),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_starts_with(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.starts_with(v[1].to_string().as_ref())
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_ends_with(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string().ends_with(v[1].to_string().as_ref()).into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_lines(mut v: Vec<Variable>) -> Variable {
|
||||
match v.remove(0) {
|
||||
Variable::String(s) => s
|
||||
.lines()
|
||||
.map(|s| Variable::from(s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
val => val,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_substring(v: Vec<Variable>) -> Variable {
|
||||
v[0].to_string()
|
||||
.chars()
|
||||
.skip(v[1].to_usize().unwrap_or_default())
|
||||
.take(v[2].to_usize().unwrap_or_default())
|
||||
.collect::<String>()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn fn_strip_prefix(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap();
|
||||
let prefix = v.next().unwrap().into_string();
|
||||
|
||||
value.transform(|s| match s {
|
||||
Cow::Borrowed(s) => s
|
||||
.strip_prefix(prefix.as_ref())
|
||||
.map(Variable::from)
|
||||
.unwrap_or_default(),
|
||||
Cow::Owned(s) => s
|
||||
.strip_prefix(prefix.as_ref())
|
||||
.map(|s| Variable::from(s.to_string()))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_strip_suffix(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap();
|
||||
let suffix = v.next().unwrap().into_string();
|
||||
|
||||
value.transform(|s| match s {
|
||||
Cow::Borrowed(s) => s
|
||||
.strip_suffix(suffix.as_ref())
|
||||
.map(Variable::from)
|
||||
.unwrap_or_default(),
|
||||
Cow::Owned(s) => s
|
||||
.strip_suffix(suffix.as_ref())
|
||||
.map(|s| Variable::from(s.to_string()))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn fn_split(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap().into_string();
|
||||
let arg = v.next().unwrap().into_string();
|
||||
|
||||
match value {
|
||||
Cow::Borrowed(s) => s
|
||||
.split(arg.as_ref())
|
||||
.map(Variable::from)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Cow::Owned(s) => s
|
||||
.split(arg.as_ref())
|
||||
.map(|s| Variable::from(s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_rsplit(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap().into_string();
|
||||
let arg = v.next().unwrap().into_string();
|
||||
|
||||
match value {
|
||||
Cow::Borrowed(s) => s
|
||||
.rsplit(arg.as_ref())
|
||||
.map(Variable::from)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
Cow::Owned(s) => s
|
||||
.rsplit(arg.as_ref())
|
||||
.map(|s| Variable::from(s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_split_once(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap().into_string();
|
||||
let arg = v.next().unwrap().into_string();
|
||||
|
||||
match value {
|
||||
Cow::Borrowed(s) => s
|
||||
.split_once(arg.as_ref())
|
||||
.map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)]))
|
||||
.unwrap_or_default(),
|
||||
Cow::Owned(s) => s
|
||||
.split_once(arg.as_ref())
|
||||
.map(|(a, b)| {
|
||||
Variable::Array(vec![
|
||||
Variable::from(a.to_string()),
|
||||
Variable::from(b.to_string()),
|
||||
])
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_rsplit_once(v: Vec<Variable>) -> Variable {
|
||||
let mut v = v.into_iter();
|
||||
let value = v.next().unwrap().into_string();
|
||||
let arg = v.next().unwrap().into_string();
|
||||
|
||||
match value {
|
||||
Cow::Borrowed(s) => s
|
||||
.rsplit_once(arg.as_ref())
|
||||
.map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)]))
|
||||
.unwrap_or_default(),
|
||||
Cow::Owned(s) => s
|
||||
.rsplit_once(arg.as_ref())
|
||||
.map(|(a, b)| {
|
||||
Variable::Array(vec![
|
||||
Variable::from(a.to_string()),
|
||||
Variable::from(b.to_string()),
|
||||
])
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
197
crates/common/src/expr/if_block.rs
Normal file
197
crates/common/src/expr/if_block.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::expr::{Constant, Expression};
|
||||
|
||||
use super::{
|
||||
parser::ExpressionParser,
|
||||
tokenizer::{TokenMap, Tokenizer},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))]
|
||||
pub struct IfThen {
|
||||
pub expr: Expression,
|
||||
pub then: Expression,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))]
|
||||
pub struct IfBlock {
|
||||
pub key: String,
|
||||
pub if_then: Vec<IfThen>,
|
||||
pub default: Expression,
|
||||
}
|
||||
|
||||
impl IfBlock {
|
||||
pub fn new<T: Into<Constant>>(value: T) -> Self {
|
||||
Self {
|
||||
key: String::new(),
|
||||
if_then: Vec::new(),
|
||||
default: Expression::from(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.default.is_empty() && self.if_then.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
pub fn try_parse(config: &mut Config, key: &str, token_map: &TokenMap) -> Option<Expression> {
|
||||
if let Some(expr) = config.value_or_warn(key) {
|
||||
match ExpressionParser::new(Tokenizer::new(expr, token_map)).parse() {
|
||||
Ok(expr) => Some(expr),
|
||||
Err(err) => {
|
||||
config.new_parse_error(key, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IfBlock {
|
||||
pub fn try_parse(
|
||||
config: &mut Config,
|
||||
prefix: impl AsKey,
|
||||
token_map: &TokenMap,
|
||||
) -> Option<IfBlock> {
|
||||
let key = prefix.as_key();
|
||||
|
||||
// Parse conditions
|
||||
let mut if_block = IfBlock {
|
||||
key,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Try first with a single value
|
||||
if config.contains_key(if_block.key.as_str()) {
|
||||
if_block.default = Expression::try_parse(config, &if_block.key, token_map)?;
|
||||
return Some(if_block);
|
||||
}
|
||||
|
||||
// Collect prefixes
|
||||
let prefix = prefix.as_prefix();
|
||||
let keys = config
|
||||
.keys
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(&prefix))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut found_if = false;
|
||||
let mut found_else = "";
|
||||
let mut found_then = false;
|
||||
let mut last_array_pos = "";
|
||||
|
||||
for item in &keys {
|
||||
let suffix_ = item.strip_prefix(&prefix).unwrap();
|
||||
|
||||
if let Some((array_pos, suffix)) = suffix_.split_once('.') {
|
||||
let if_key = suffix.split_once('.').map(|(v, _)| v).unwrap_or(suffix);
|
||||
if if_key == "if" {
|
||||
if array_pos != last_array_pos {
|
||||
if !last_array_pos.is_empty() && !found_then {
|
||||
config.new_parse_error(
|
||||
if_block.key,
|
||||
format!(
|
||||
"Missing 'then' in 'if' condition {}.",
|
||||
last_array_pos.parse().unwrap_or(0) + 1,
|
||||
),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
if_block.if_then.push(IfThen {
|
||||
expr: Expression::try_parse(config, item, token_map)?,
|
||||
then: Expression::default(),
|
||||
});
|
||||
|
||||
found_then = false;
|
||||
last_array_pos = array_pos;
|
||||
}
|
||||
|
||||
found_if = true;
|
||||
} else if if_key == "else" {
|
||||
if found_else.is_empty() {
|
||||
if found_if {
|
||||
if_block.default = Expression::try_parse(config, item, token_map)?;
|
||||
found_else = array_pos;
|
||||
} else {
|
||||
config.new_parse_error(if_block.key, "Found 'else' before 'if'");
|
||||
return None;
|
||||
}
|
||||
} else if array_pos != found_else {
|
||||
config.new_parse_error(if_block.key, "Multiple 'else' found");
|
||||
return None;
|
||||
}
|
||||
} else if if_key == "then" {
|
||||
if found_else.is_empty() {
|
||||
if array_pos == last_array_pos {
|
||||
if !found_then {
|
||||
if_block.if_then.last_mut().unwrap().then =
|
||||
Expression::try_parse(config, item, token_map)?;
|
||||
found_then = true;
|
||||
}
|
||||
} else {
|
||||
config.new_parse_error(if_block.key, "Found 'then' without 'if'");
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
config.new_parse_error(if_block.key, "Found 'then' in 'else' block");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
config.new_parse_error(
|
||||
if_block.key,
|
||||
format!("Invalid property {item:?} found in 'if' block."),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_if {
|
||||
config.new_missing_property(if_block.key);
|
||||
None
|
||||
} else if !found_then {
|
||||
config.new_parse_error(
|
||||
if_block.key,
|
||||
format!(
|
||||
"Missing 'then' in 'if' condition {}",
|
||||
last_array_pos.parse().unwrap_or(0) + 1,
|
||||
),
|
||||
);
|
||||
None
|
||||
} else if found_else.is_empty() {
|
||||
config.new_parse_error(if_block.key, "Missing 'else'");
|
||||
None
|
||||
} else {
|
||||
Some(if_block)
|
||||
}
|
||||
}
|
||||
}
|
||||
315
crates/common/src/expr/mod.rs
Normal file
315
crates/common/src/expr/mod.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
|
||||
use regex::Regex;
|
||||
use utils::config::utils::ParseValue;
|
||||
|
||||
pub mod eval;
|
||||
pub mod functions;
|
||||
pub mod if_block;
|
||||
pub mod parser;
|
||||
pub mod tokenizer;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct Expression {
|
||||
pub items: Vec<ExpressionItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExpressionItem {
|
||||
Variable(u32),
|
||||
Capture(u32),
|
||||
Constant(Constant),
|
||||
BinaryOperator(BinaryOperator),
|
||||
UnaryOperator(UnaryOperator),
|
||||
Regex(Regex),
|
||||
JmpIf { val: bool, pos: u32 },
|
||||
Function { id: u32, num_args: u32 },
|
||||
ArrayAccess,
|
||||
ArrayBuild(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Variable<'x> {
|
||||
String(Cow<'x, str>),
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
Array(Vec<Variable<'x>>),
|
||||
}
|
||||
|
||||
impl Default for Variable<'_> {
|
||||
fn default() -> Self {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Constant {
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Eq for Constant {}
|
||||
|
||||
impl From<String> for Constant {
|
||||
fn from(value: String) -> Self {
|
||||
Constant::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Constant {
|
||||
fn from(value: bool) -> Self {
|
||||
Constant::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Constant {
|
||||
fn from(value: i64) -> Self {
|
||||
Constant::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Constant {
|
||||
fn from(value: i32) -> Self {
|
||||
Constant::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Constant {
|
||||
fn from(value: i16) -> Self {
|
||||
Constant::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Constant {
|
||||
fn from(value: f64) -> Self {
|
||||
Constant::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Constant {
|
||||
fn from(value: usize) -> Self {
|
||||
Constant::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum BinaryOperator {
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum UnaryOperator {
|
||||
Not,
|
||||
Minus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Token {
|
||||
Variable(u32),
|
||||
Capture(u32),
|
||||
Function {
|
||||
name: Cow<'static, str>,
|
||||
id: u32,
|
||||
num_args: u32,
|
||||
},
|
||||
Constant(Constant),
|
||||
Regex(Regex),
|
||||
BinaryOperator(BinaryOperator),
|
||||
UnaryOperator(UnaryOperator),
|
||||
OpenParen,
|
||||
CloseParen,
|
||||
OpenBracket,
|
||||
CloseBracket,
|
||||
Comma,
|
||||
}
|
||||
|
||||
impl From<usize> for Variable<'_> {
|
||||
fn from(value: usize) -> Self {
|
||||
Variable::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Variable<'_> {
|
||||
fn from(value: i64) -> Self {
|
||||
Variable::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Variable<'_> {
|
||||
fn from(value: i32) -> Self {
|
||||
Variable::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Variable<'_> {
|
||||
fn from(value: i16) -> Self {
|
||||
Variable::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Variable<'_> {
|
||||
fn from(value: f64) -> Self {
|
||||
Variable::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<&'x str> for Variable<'x> {
|
||||
fn from(value: &'x str) -> Self {
|
||||
Variable::String(Cow::Borrowed(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Variable<'_> {
|
||||
fn from(value: String) -> Self {
|
||||
Variable::String(Cow::Owned(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<Vec<Variable<'x>>> for Variable<'x> {
|
||||
fn from(value: Vec<Variable<'x>>) -> Self {
|
||||
Variable::Array(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Variable<'_> {
|
||||
fn from(value: bool) -> Self {
|
||||
Variable::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Constant>> From<T> for Expression {
|
||||
fn from(value: T) -> Self {
|
||||
Expression {
|
||||
items: vec![ExpressionItem::Constant(value.into())],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ExpressionItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Variable(l0), Self::Variable(r0)) => l0 == r0,
|
||||
(Self::Constant(l0), Self::Constant(r0)) => l0 == r0,
|
||||
(Self::BinaryOperator(l0), Self::BinaryOperator(r0)) => l0 == r0,
|
||||
(Self::UnaryOperator(l0), Self::UnaryOperator(r0)) => l0 == r0,
|
||||
(Self::Regex(_), Self::Regex(_)) => true,
|
||||
(
|
||||
Self::JmpIf {
|
||||
val: l_val,
|
||||
pos: l_pos,
|
||||
},
|
||||
Self::JmpIf {
|
||||
val: r_val,
|
||||
pos: r_pos,
|
||||
},
|
||||
) => l_val == r_val && l_pos == r_pos,
|
||||
(
|
||||
Self::Function {
|
||||
id: l_id,
|
||||
num_args: l_num_args,
|
||||
},
|
||||
Self::Function {
|
||||
id: r_id,
|
||||
num_args: r_num_args,
|
||||
},
|
||||
) => l_id == r_id && l_num_args == r_num_args,
|
||||
(Self::ArrayBuild(l0), Self::ArrayBuild(r0)) => l0 == r0,
|
||||
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ExpressionItem {}
|
||||
|
||||
impl PartialEq for Token {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Variable(l0), Self::Variable(r0)) => l0 == r0,
|
||||
(
|
||||
Self::Function {
|
||||
name: l_name,
|
||||
id: l_id,
|
||||
num_args: l_num_args,
|
||||
},
|
||||
Self::Function {
|
||||
name: r_name,
|
||||
id: r_id,
|
||||
num_args: r_num_args,
|
||||
},
|
||||
) => l_name == r_name && l_id == r_id && l_num_args == r_num_args,
|
||||
(Self::Constant(l0), Self::Constant(r0)) => l0 == r0,
|
||||
(Self::Regex(_), Self::Regex(_)) => true,
|
||||
(Self::BinaryOperator(l0), Self::BinaryOperator(r0)) => l0 == r0,
|
||||
(Self::UnaryOperator(l0), Self::UnaryOperator(r0)) => l0 == r0,
|
||||
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Token {}
|
||||
|
||||
pub trait ConstantValue:
|
||||
ParseValue + for<'x> TryFrom<Variable<'x>> + Into<Constant> + Sized
|
||||
{
|
||||
}
|
||||
|
||||
impl ConstantValue for Duration {}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for Duration {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Integer(value) if value > 0 => Ok(Duration::from_millis(value as u64)),
|
||||
Variable::Float(value) if value > 0.0 => Ok(Duration::from_millis(value as u64)),
|
||||
Variable::String(value) if !value.is_empty() => {
|
||||
Duration::parse_value("", &value).map_err(|_| ())
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for Constant {
|
||||
fn from(value: Duration) -> Self {
|
||||
Constant::Integer(value.as_millis() as i64)
|
||||
}
|
||||
}
|
||||
287
crates/common/src/expr/parser.rs
Normal file
287
crates/common/src/expr/parser.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use super::{tokenizer::Tokenizer, BinaryOperator, Expression, ExpressionItem, Token};
|
||||
|
||||
pub struct ExpressionParser<'x> {
|
||||
pub(crate) tokenizer: Tokenizer<'x>,
|
||||
pub(crate) output: Vec<ExpressionItem>,
|
||||
operator_stack: Vec<(Token, Option<usize>)>,
|
||||
arg_count: Vec<i32>,
|
||||
}
|
||||
|
||||
pub(crate) const ID_ARRAY_ACCESS: u32 = u32::MAX;
|
||||
pub(crate) const ID_ARRAY_BUILD: u32 = u32::MAX - 1;
|
||||
|
||||
impl<'x> ExpressionParser<'x> {
|
||||
pub fn new(tokenizer: Tokenizer<'x>) -> Self {
|
||||
Self {
|
||||
tokenizer,
|
||||
output: Vec::new(),
|
||||
operator_stack: Vec::new(),
|
||||
arg_count: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Result<Expression, String> {
|
||||
let mut last_is_var_or_fnc = false;
|
||||
|
||||
while let Some(token) = self.tokenizer.next()? {
|
||||
let mut is_var_or_fnc = false;
|
||||
match token {
|
||||
Token::Variable(v) => {
|
||||
self.inc_arg_count();
|
||||
is_var_or_fnc = true;
|
||||
self.output.push(ExpressionItem::Variable(v))
|
||||
}
|
||||
Token::Constant(c) => {
|
||||
self.inc_arg_count();
|
||||
self.output.push(ExpressionItem::Constant(c))
|
||||
}
|
||||
Token::Capture(c) => {
|
||||
self.inc_arg_count();
|
||||
self.output.push(ExpressionItem::Capture(c))
|
||||
}
|
||||
Token::UnaryOperator(uop) => {
|
||||
self.operator_stack.push((Token::UnaryOperator(uop), None))
|
||||
}
|
||||
Token::OpenParen => self.operator_stack.push((token, None)),
|
||||
Token::CloseParen | Token::CloseBracket => {
|
||||
let expect_token = if matches!(token, Token::CloseParen) {
|
||||
Token::OpenParen
|
||||
} else {
|
||||
Token::OpenBracket
|
||||
};
|
||||
loop {
|
||||
match self.operator_stack.pop() {
|
||||
Some((t, _)) if t == expect_token => {
|
||||
break;
|
||||
}
|
||||
Some((Token::BinaryOperator(bop), jmp_pos)) => {
|
||||
self.update_jmp_pos(jmp_pos);
|
||||
self.output.push(ExpressionItem::BinaryOperator(bop))
|
||||
}
|
||||
Some((Token::UnaryOperator(uop), _)) => {
|
||||
self.output.push(ExpressionItem::UnaryOperator(uop))
|
||||
}
|
||||
_ => return Err("Mismatched parentheses".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
match self.operator_stack.last() {
|
||||
Some((Token::Function { id, num_args, name }, _)) => {
|
||||
let got_args = self.arg_count.pop().unwrap();
|
||||
if got_args != *num_args as i32 {
|
||||
return Err(if *id != u32::MAX {
|
||||
format!(
|
||||
"Expression function {:?} expected {} arguments, got {}",
|
||||
name, num_args, got_args
|
||||
)
|
||||
} else {
|
||||
"Missing array index".to_string()
|
||||
});
|
||||
}
|
||||
|
||||
let expr = match *id {
|
||||
ID_ARRAY_ACCESS => ExpressionItem::ArrayAccess,
|
||||
ID_ARRAY_BUILD => ExpressionItem::ArrayBuild(*num_args),
|
||||
id => ExpressionItem::Function {
|
||||
id,
|
||||
num_args: *num_args,
|
||||
},
|
||||
};
|
||||
|
||||
self.operator_stack.pop();
|
||||
self.output.push(expr);
|
||||
}
|
||||
Some((Token::Regex(regex), _)) => {
|
||||
if self.arg_count.pop().unwrap() != 1 {
|
||||
return Err("Expression function \"matches\" expected 2 arguments"
|
||||
.to_string());
|
||||
}
|
||||
self.output.push(ExpressionItem::Regex(regex.clone()));
|
||||
self.operator_stack.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_var_or_fnc = true;
|
||||
}
|
||||
Token::BinaryOperator(bop) => {
|
||||
self.dec_arg_count();
|
||||
while let Some((top_token, prev_jmp_pos)) = self.operator_stack.last() {
|
||||
match top_token {
|
||||
Token::BinaryOperator(top_bop) => {
|
||||
if bop.precedence() <= top_bop.precedence() {
|
||||
let top_bop = *top_bop;
|
||||
let jmp_pos = *prev_jmp_pos;
|
||||
self.update_jmp_pos(jmp_pos);
|
||||
self.operator_stack.pop();
|
||||
self.output.push(ExpressionItem::BinaryOperator(top_bop));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Token::UnaryOperator(top_uop) => {
|
||||
let top_uop = *top_uop;
|
||||
self.operator_stack.pop();
|
||||
self.output.push(ExpressionItem::UnaryOperator(top_uop));
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Add jump instruction for short-circuiting
|
||||
let jmp_pos = match bop {
|
||||
BinaryOperator::And => {
|
||||
self.output
|
||||
.push(ExpressionItem::JmpIf { val: false, pos: 0 });
|
||||
Some(self.output.len() - 1)
|
||||
}
|
||||
BinaryOperator::Or => {
|
||||
self.output
|
||||
.push(ExpressionItem::JmpIf { val: true, pos: 0 });
|
||||
Some(self.output.len() - 1)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
self.operator_stack
|
||||
.push((Token::BinaryOperator(bop), jmp_pos));
|
||||
}
|
||||
Token::Function { id, name, num_args } => {
|
||||
self.inc_arg_count();
|
||||
self.arg_count.push(0);
|
||||
self.operator_stack
|
||||
.push((Token::Function { id, name, num_args }, None))
|
||||
}
|
||||
Token::Regex(regex) => {
|
||||
self.inc_arg_count();
|
||||
self.arg_count.push(0);
|
||||
self.operator_stack.push((Token::Regex(regex), None))
|
||||
}
|
||||
Token::OpenBracket => {
|
||||
// Array functions
|
||||
let (id, num_args, arg_count) = if last_is_var_or_fnc {
|
||||
(ID_ARRAY_ACCESS, 2, 1)
|
||||
} else {
|
||||
self.inc_arg_count();
|
||||
(ID_ARRAY_BUILD, 0, 0)
|
||||
};
|
||||
self.arg_count.push(arg_count);
|
||||
self.operator_stack.push((
|
||||
Token::Function {
|
||||
id,
|
||||
name: "array".into(),
|
||||
num_args,
|
||||
},
|
||||
None,
|
||||
));
|
||||
self.operator_stack.push((token, None));
|
||||
}
|
||||
Token::Comma => {
|
||||
while let Some((token, jmp_pos)) = self.operator_stack.last() {
|
||||
match token {
|
||||
Token::OpenParen => break,
|
||||
Token::BinaryOperator(bop) => {
|
||||
let bop = *bop;
|
||||
let jmp_pos = *jmp_pos;
|
||||
self.update_jmp_pos(jmp_pos);
|
||||
self.output.push(ExpressionItem::BinaryOperator(bop));
|
||||
self.operator_stack.pop();
|
||||
}
|
||||
Token::UnaryOperator(uop) => {
|
||||
self.output.push(ExpressionItem::UnaryOperator(*uop));
|
||||
self.operator_stack.pop();
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
last_is_var_or_fnc = is_var_or_fnc;
|
||||
}
|
||||
|
||||
while let Some((token, jmp_pos)) = self.operator_stack.pop() {
|
||||
match token {
|
||||
Token::BinaryOperator(bop) => {
|
||||
self.update_jmp_pos(jmp_pos);
|
||||
self.output.push(ExpressionItem::BinaryOperator(bop))
|
||||
}
|
||||
Token::UnaryOperator(uop) => self.output.push(ExpressionItem::UnaryOperator(uop)),
|
||||
_ => return Err("Invalid token on the operator stack".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
if self.operator_stack.is_empty() {
|
||||
Ok(Expression { items: self.output })
|
||||
} else {
|
||||
Err("Invalid expression".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn inc_arg_count(&mut self) {
|
||||
if let Some(x) = self.arg_count.last_mut() {
|
||||
*x = x.saturating_add(1);
|
||||
let op_pos = self.operator_stack.len().saturating_sub(2);
|
||||
match self.operator_stack.get_mut(op_pos) {
|
||||
Some((Token::Function { num_args, id, .. }, _)) if *id == ID_ARRAY_BUILD => {
|
||||
*num_args += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dec_arg_count(&mut self) {
|
||||
if let Some(x) = self.arg_count.last_mut() {
|
||||
*x = x.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_jmp_pos(&mut self, jmp_pos: Option<usize>) {
|
||||
if let Some(jmp_pos) = jmp_pos {
|
||||
let cur_pos = self.output.len();
|
||||
if let ExpressionItem::JmpIf { pos, .. } = &mut self.output[jmp_pos] {
|
||||
*pos = (cur_pos - jmp_pos) as u32;
|
||||
} else {
|
||||
#[cfg(test)]
|
||||
panic!("Invalid jump position");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryOperator {
|
||||
fn precedence(&self) -> i32 {
|
||||
match self {
|
||||
BinaryOperator::Multiply | BinaryOperator::Divide => 7,
|
||||
BinaryOperator::Add | BinaryOperator::Subtract => 6,
|
||||
BinaryOperator::Gt | BinaryOperator::Ge | BinaryOperator::Lt | BinaryOperator::Le => 5,
|
||||
BinaryOperator::Eq | BinaryOperator::Ne => 4,
|
||||
BinaryOperator::Xor => 3,
|
||||
BinaryOperator::And => 2,
|
||||
BinaryOperator::Or => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
373
crates/common/src/expr/tokenizer.rs
Normal file
373
crates/common/src/expr/tokenizer.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, iter::Peekable, slice::Iter, time::Duration};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use regex::Regex;
|
||||
use utils::config::utils::ParseValue;
|
||||
|
||||
use super::{
|
||||
functions::{ASYNC_FUNCTIONS, FUNCTIONS},
|
||||
BinaryOperator, Constant, Token, UnaryOperator,
|
||||
};
|
||||
|
||||
pub struct Tokenizer<'x> {
|
||||
pub(crate) iter: Peekable<Iter<'x, u8>>,
|
||||
token_map: &'x TokenMap,
|
||||
buf: Vec<u8>,
|
||||
depth: u32,
|
||||
next_token: Vec<Token>,
|
||||
has_number: bool,
|
||||
has_dot: bool,
|
||||
has_alpha: bool,
|
||||
is_start: bool,
|
||||
is_eof: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TokenMap {
|
||||
tokens: AHashMap<&'static str, Token>,
|
||||
}
|
||||
|
||||
impl<'x> Tokenizer<'x> {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn new(expr: &'x str, token_map: &'x TokenMap) -> Self {
|
||||
Self {
|
||||
iter: expr.as_bytes().iter().peekable(),
|
||||
buf: Vec::new(),
|
||||
depth: 0,
|
||||
next_token: Vec::with_capacity(2),
|
||||
has_number: false,
|
||||
has_dot: false,
|
||||
has_alpha: false,
|
||||
is_start: true,
|
||||
is_eof: false,
|
||||
token_map,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Result<Option<Token>, String> {
|
||||
if let Some(token) = self.next_token.pop() {
|
||||
return Ok(Some(token));
|
||||
} else if self.is_eof {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
while let Some(&ch) = self.iter.next() {
|
||||
match ch {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'$' => {
|
||||
self.buf.push(ch);
|
||||
self.has_alpha = true;
|
||||
}
|
||||
b'0'..=b'9' => {
|
||||
self.buf.push(ch);
|
||||
self.has_number = true;
|
||||
}
|
||||
b'.' => {
|
||||
self.buf.push(ch);
|
||||
self.has_dot = true;
|
||||
}
|
||||
b'}' => {
|
||||
self.is_eof = true;
|
||||
break;
|
||||
}
|
||||
b'-' if self.buf.last().map_or(false, |c| *c == b'[') => {
|
||||
self.buf.push(ch);
|
||||
}
|
||||
b':' if self.buf.contains(&b'.') => {
|
||||
self.buf.push(ch);
|
||||
}
|
||||
b']' if self.buf.contains(&b'[') => {
|
||||
self.buf.push(b']');
|
||||
}
|
||||
b'*' if self.buf.last().map_or(false, |&c| c == b'[' || c == b'.') => {
|
||||
self.buf.push(ch);
|
||||
}
|
||||
_ => {
|
||||
let (prev_token, ch) = if ch == b'(' && self.buf.eq(b"matches") {
|
||||
// Parse regular expressions
|
||||
let stop_ch = self.find_char(&[b'\"', b'\''])?;
|
||||
let regex_str = self.parse_string(stop_ch)?;
|
||||
let regex = Regex::new(®ex_str).map_err(|e| {
|
||||
format!("Invalid regular expression {:?}: {}", regex_str, e)
|
||||
})?;
|
||||
self.has_alpha = false;
|
||||
self.buf.clear();
|
||||
self.find_char(&[b','])?;
|
||||
(Token::Regex(regex).into(), b'(')
|
||||
} else if !self.buf.is_empty() {
|
||||
self.is_start = false;
|
||||
(self.parse_buf()?.into(), ch)
|
||||
} else {
|
||||
(None, ch)
|
||||
};
|
||||
let token = match ch {
|
||||
b'&' => {
|
||||
if matches!(self.iter.peek(), Some(b'&')) {
|
||||
self.iter.next();
|
||||
}
|
||||
Token::BinaryOperator(BinaryOperator::And)
|
||||
}
|
||||
b'|' => {
|
||||
if matches!(self.iter.peek(), Some(b'|')) {
|
||||
self.iter.next();
|
||||
}
|
||||
Token::BinaryOperator(BinaryOperator::Or)
|
||||
}
|
||||
b'!' => {
|
||||
if matches!(self.iter.peek(), Some(b'=')) {
|
||||
self.iter.next();
|
||||
Token::BinaryOperator(BinaryOperator::Ne)
|
||||
} else {
|
||||
Token::UnaryOperator(UnaryOperator::Not)
|
||||
}
|
||||
}
|
||||
b'^' => Token::BinaryOperator(BinaryOperator::Xor),
|
||||
b'(' => {
|
||||
self.depth += 1;
|
||||
Token::OpenParen
|
||||
}
|
||||
b')' => {
|
||||
if self.depth == 0 {
|
||||
return Err("Unmatched close parenthesis".to_string());
|
||||
}
|
||||
self.depth -= 1;
|
||||
Token::CloseParen
|
||||
}
|
||||
b'+' => Token::BinaryOperator(BinaryOperator::Add),
|
||||
b'*' => Token::BinaryOperator(BinaryOperator::Multiply),
|
||||
b'/' => Token::BinaryOperator(BinaryOperator::Divide),
|
||||
b'-' => {
|
||||
if self.is_start {
|
||||
Token::UnaryOperator(UnaryOperator::Minus)
|
||||
} else {
|
||||
Token::BinaryOperator(BinaryOperator::Subtract)
|
||||
}
|
||||
}
|
||||
b'=' => match self.iter.next() {
|
||||
Some(b'=') => Token::BinaryOperator(BinaryOperator::Eq),
|
||||
Some(b'>') => Token::BinaryOperator(BinaryOperator::Ge),
|
||||
Some(b'<') => Token::BinaryOperator(BinaryOperator::Le),
|
||||
_ => Token::BinaryOperator(BinaryOperator::Eq),
|
||||
},
|
||||
b'>' => match self.iter.peek() {
|
||||
Some(b'=') => {
|
||||
self.iter.next();
|
||||
Token::BinaryOperator(BinaryOperator::Ge)
|
||||
}
|
||||
_ => Token::BinaryOperator(BinaryOperator::Gt),
|
||||
},
|
||||
b'<' => match self.iter.peek() {
|
||||
Some(b'=') => {
|
||||
self.iter.next();
|
||||
Token::BinaryOperator(BinaryOperator::Le)
|
||||
}
|
||||
_ => Token::BinaryOperator(BinaryOperator::Lt),
|
||||
},
|
||||
b',' => Token::Comma,
|
||||
b'[' => Token::OpenBracket,
|
||||
b']' => Token::CloseBracket,
|
||||
b' ' | b'\r' | b'\n' => {
|
||||
if prev_token.is_some() {
|
||||
return Ok(prev_token);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
b'\"' | b'\'' => Token::Constant(Constant::String(self.parse_string(ch)?)),
|
||||
_ => {
|
||||
return Err(format!("Invalid character {:?}", char::from(ch),));
|
||||
}
|
||||
};
|
||||
self.is_start = matches!(
|
||||
token,
|
||||
Token::OpenParen | Token::Comma | Token::BinaryOperator(_)
|
||||
);
|
||||
|
||||
return if prev_token.is_some() {
|
||||
self.next_token.push(token);
|
||||
Ok(prev_token)
|
||||
} else {
|
||||
Ok(Some(token))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.depth > 0 {
|
||||
Err("Unmatched open parenthesis".to_string())
|
||||
} else if !self.buf.is_empty() {
|
||||
self.parse_buf().map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_char(&mut self, chars: &[u8]) -> Result<u8, String> {
|
||||
for &ch in self.iter.by_ref() {
|
||||
if !ch.is_ascii_whitespace() {
|
||||
return if chars.contains(&ch) {
|
||||
Ok(ch)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Expected {:?}, found invalid character {:?}",
|
||||
char::from(chars[0]),
|
||||
char::from(ch),
|
||||
))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Err("Unexpected end of expression".to_string())
|
||||
}
|
||||
|
||||
fn parse_string(&mut self, stop_ch: u8) -> Result<String, String> {
|
||||
let mut buf = Vec::with_capacity(16);
|
||||
let mut last_ch = 0;
|
||||
let mut found_end = false;
|
||||
|
||||
for &ch in self.iter.by_ref() {
|
||||
if last_ch != b'\\' {
|
||||
if ch != stop_ch {
|
||||
buf.push(ch);
|
||||
} else {
|
||||
found_end = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
match ch {
|
||||
b'n' => {
|
||||
buf.push(b'\n');
|
||||
}
|
||||
b'r' => {
|
||||
buf.push(b'\r');
|
||||
}
|
||||
b't' => {
|
||||
buf.push(b'\t');
|
||||
}
|
||||
_ => {
|
||||
buf.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if found_end {
|
||||
String::from_utf8(buf).map_err(|_| "Invalid UTF-8".to_string())
|
||||
} else {
|
||||
Err("Unterminated string".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_buf(&mut self) -> Result<Token, String> {
|
||||
let buf = String::from_utf8(std::mem::take(&mut self.buf)).unwrap_or_default();
|
||||
if self.has_number && !self.has_alpha {
|
||||
self.has_number = false;
|
||||
if self.has_dot {
|
||||
self.has_dot = false;
|
||||
|
||||
buf.parse::<f64>()
|
||||
.map(|f| Token::Constant(Constant::Float(f)))
|
||||
.map_err(|_| format!("Invalid float value {}", buf,))
|
||||
} else {
|
||||
buf.parse::<i64>()
|
||||
.map(|i| Token::Constant(Constant::Integer(i)))
|
||||
.map_err(|_| format!("Invalid integer value {}", buf,))
|
||||
}
|
||||
} else {
|
||||
let has_dot = self.has_dot;
|
||||
let has_number = self.has_number;
|
||||
|
||||
self.has_alpha = false;
|
||||
self.has_number = false;
|
||||
self.has_dot = false;
|
||||
|
||||
if !has_number && !has_dot && [4, 5].contains(&buf.len()) {
|
||||
if buf == "true" {
|
||||
return Ok(Token::Constant(Constant::Integer(1)));
|
||||
} else if buf == "false" {
|
||||
return Ok(Token::Constant(Constant::Integer(0)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(regex_capture) = buf.strip_prefix('$').and_then(|v| v.parse::<u32>().ok()) {
|
||||
Ok(Token::Capture(regex_capture))
|
||||
} else if let Some((idx, (name, _, num_args))) = FUNCTIONS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, (name, _, _))| name == &buf)
|
||||
{
|
||||
Ok(Token::Function {
|
||||
name: Cow::Borrowed(*name),
|
||||
id: idx as u32,
|
||||
num_args: *num_args,
|
||||
})
|
||||
} else if let Some((name, idx, num_args)) =
|
||||
ASYNC_FUNCTIONS.iter().find(|(name, _, _)| name == &buf)
|
||||
{
|
||||
Ok(Token::Function {
|
||||
name: Cow::Borrowed(*name),
|
||||
id: *idx + FUNCTIONS.len() as u32,
|
||||
num_args: *num_args,
|
||||
})
|
||||
} else if let Some(token) = self.token_map.tokens.get(buf.as_str()) {
|
||||
Ok(token.clone())
|
||||
} else if let Ok(duration) = Duration::parse_value("", &buf) {
|
||||
Ok(Token::Constant(Constant::Integer(
|
||||
duration.as_millis() as i64
|
||||
)))
|
||||
} else {
|
||||
Err(format!("Invalid variable or constant {buf:?}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenMap {
|
||||
pub fn with_variables<I>(mut self, vars: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (&'static str, u32)>,
|
||||
{
|
||||
for (name, idx) in vars {
|
||||
self.tokens.insert(name, Token::Variable(idx));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_constants<I, T>(mut self, consts: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (&'static str, T)>,
|
||||
T: Into<Constant>,
|
||||
{
|
||||
for (name, constant) in consts {
|
||||
self.tokens.insert(name, Token::Constant(constant.into()));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user