Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
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();
} else {
arr.sort_unstable_by(|a, b| b.cmp(a));
}
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,
}
}
+385
View File
@@ -0,0 +1,385 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::*;
use crate::{Server, expr::StringCow};
use compact_str::{CompactString, ToCompactString};
use mail_auth::IpLookupStrategy;
use std::{cmp::Ordering, net::IpAddr, vec::IntoIter};
use store::{Deserialize, Rows, Value, dispatch::lookup::KeyValue};
use trc::AddContext;
impl Server {
pub(crate) async fn eval_fnc<'x>(
&self,
fnc_id: u32,
params: Vec<Variable<'x>>,
session_id: u64,
) -> trc::Result<Variable<'x>> {
let mut params = FncParams::new(params);
match fnc_id {
F_IS_LOCAL_DOMAIN => {
let domain = params.next_as_string();
self.domain(domain.as_str())
.await
.caused_by(trc::location!())
.map(|v| v.is_some().into())
}
F_IS_LOCAL_ADDRESS => {
let address = params.next_as_string();
self.rcpt_id_from_email(address.as_ref())
.await
.caused_by(trc::location!())
.map(|v| v.is_some().into())
}
F_KEY_GET => {
let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else {
return Ok(Variable::default());
};
let key = params.next_as_string();
store
.key_get::<VariableWrapper>(key.as_str())
.await
.map(|value| value.map(|v| v.into_inner()).unwrap_or_default())
.caused_by(trc::location!())
}
F_KEY_EXISTS => {
let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else {
return Ok(Variable::default());
};
let key = params.next_as_string();
store
.key_exists(key.as_str())
.await
.caused_by(trc::location!())
.map(|v| v.into())
}
F_KEY_SET => {
let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else {
return Ok(Variable::default());
};
let key = params.next_as_string();
let value = params.next_as_string();
store
.key_set(KeyValue::new(
key.as_bytes().to_vec(),
value.as_bytes().to_vec(),
))
.await
.map(|_| true)
.caused_by(trc::location!())
.map(|v| v.into())
}
F_COUNTER_INCR => {
let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else {
return Ok(Variable::default());
};
let key = params.next_as_string();
let value = params.next_as_integer();
store
.counter_incr(KeyValue::new(key.into_owned(), value), true)
.await
.map(Variable::Integer)
.caused_by(trc::location!())
}
F_COUNTER_GET => {
let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else {
return Ok(Variable::default());
};
let key = params.next_as_string();
store
.counter_get(key.as_bytes().to_vec())
.await
.map(Variable::Integer)
.caused_by(trc::location!())
}
F_DNS_QUERY => self.dns_query(params).await,
F_SQL_QUERY => self.sql_query(params, session_id).await,
_ => Ok(Variable::default()),
}
}
async fn sql_query<'x>(
&self,
mut arguments: FncParams<'x>,
session_id: u64,
) -> trc::Result<Variable<'x>> {
let store_name = arguments.next_as_string();
let Some(store) = self
.get_lookup_store(store_name.as_ref())
.and_then(|v| v.into_store())
else {
return Err(trc::EventType::Eval(trc::EvalEvent::Error)
.into_err()
.id(store_name.into_owned())
.span_id(session_id)
.details("Store not found or is not a SQL store"));
};
let query = arguments.next_as_string();
if query.is_empty() {
return Err(trc::EventType::Eval(trc::EvalEvent::Error)
.into_err()
.details("Empty query string")
.span_id(session_id));
}
// 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)
.is_some_and(|q| q.eq_ignore_ascii_case(b"SELECT"))
{
let mut rows = store
.sql_query::<Rows>(query.as_str(), arguments)
.await
.caused_by(trc::location!())?;
Ok(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 {
store
.sql_query::<usize>(query.as_str(), arguments)
.await
.caused_by(trc::location!())
.map(|v| v.into())
}
}
async fn dns_query<'x>(&self, mut arguments: FncParams<'x>) -> trc::Result<Variable<'x>> {
let entry = arguments.next_as_string();
let record_type = arguments.next_as_string();
if record_type.as_str().eq_ignore_ascii_case("ip") {
self.core
.smtp
.resolvers
.dns
.ip_lookup(
entry.as_ref(),
IpLookupStrategy::Ipv4thenIpv6,
10,
Some(&self.inner.cache.dns_ipv4),
Some(&self.inner.cache.dns_ipv6),
)
.await
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| {
result
.iter()
.map(|ip| Variable::from(ip.to_compact_string()))
.collect::<Vec<_>>()
.into()
})
} else if record_type.as_str().eq_ignore_ascii_case("mx") {
self.core
.smtp
.resolvers
.dns
.mx_lookup(entry.as_str(), Some(&self.inner.cache.dns_mx))
.await
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| {
result
.rrset
.iter()
.flat_map(|mx| {
mx.exchanges.iter().map(|host| {
Variable::String(StringCow::Owned(
host.strip_suffix('.').unwrap_or(host).to_compact_string(),
))
})
})
.collect::<Vec<_>>()
.into()
})
} else if record_type.as_str().eq_ignore_ascii_case("txt") {
self.core
.smtp
.resolvers
.dns
.txt_raw_lookup(entry.as_str())
.await
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| Variable::from(CompactString::from_utf8(result).unwrap_or_default()))
} else if record_type.as_str().eq_ignore_ascii_case("ptr") {
self.core
.smtp
.resolvers
.dns
.ptr_lookup(
entry.as_str().parse::<IpAddr>().map_err(|err| {
trc::EventType::Eval(trc::EvalEvent::Error)
.into_err()
.details("Failed to parse IP address")
.reason(err)
})?,
Some(&self.inner.cache.dns_ptr),
)
.await
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| {
result
.rrset
.iter()
.map(|host| Variable::from(host.to_compact_string()))
.collect::<Vec<_>>()
.into()
})
} else if record_type.as_str().eq_ignore_ascii_case("ipv4") {
self.core
.smtp
.resolvers
.dns
.ipv4_lookup(entry.as_str(), Some(&self.inner.cache.dns_ipv4))
.await
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| {
result
.rrset
.iter()
.map(|ip| Variable::from(ip.to_compact_string()))
.collect::<Vec<_>>()
.into()
})
} else if record_type.as_str().eq_ignore_ascii_case("ipv6") {
self.core
.smtp
.resolvers
.dns
.ipv6_lookup(entry.as_str(), Some(&self.inner.cache.dns_ipv6))
.await
.map_err(|err| trc::Error::from(err).caused_by(trc::location!()))
.map(|result| {
result
.rrset
.iter()
.map(|ip| Variable::from(ip.to_compact_string()))
.collect::<Vec<_>>()
.into()
})
} else {
Ok(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) -> StringCow<'x> {
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]) -> trc::Result<Self> {
Ok(VariableWrapper(Variable::String(StringCow::Owned(
CompactString::from_utf8_lossy(bytes),
))))
}
}
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(StringCow::Owned(v.into())),
Value::Blob(v) => Variable::String(StringCow::Owned(match v {
std::borrow::Cow::Borrowed(v) => CompactString::from_utf8_lossy(v),
std::borrow::Cow::Owned(v) => CompactString::from_utf8_lossy(&v),
})),
Value::Null => Variable::String(StringCow::Borrowed("")),
})
}
}
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.to_string().into()),
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.into()),
Value::Blob(v) => Variable::String(StringCow::Owned(CompactString::from_utf8_lossy(&v))),
Value::Null => Variable::default(),
}
}
+104
View File
@@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::CompactString;
use crate::expr::{StringCow, 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().as_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 {
StringCow::Borrowed(s) => s
.rsplit_once('@')
.map(|(u, d)| match part.as_str() {
"local" => Variable::from(u.trim()),
"domain" => Variable::from(d.trim()),
_ => Variable::default(),
})
.unwrap_or_default(),
StringCow::Owned(s) => s
.rsplit_once('@')
.map(|(u, d)| match part.as_str() {
"local" => Variable::from(CompactString::new(u.trim())),
"domain" => Variable::from(CompactString::new(d.trim())),
_ => Variable::default(),
})
.unwrap_or_default(),
})
}
+78
View File
@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::expr::Variable;
use compact_str::CompactString;
use mail_auth::common::resolver::ToReverseName;
use registry::types::ipmask::IpAddrOrMask;
use std::{net::IpAddr, str::FromStr};
pub(crate) fn fn_is_empty(v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.is_empty(),
Variable::Integer(_) | Variable::Float(_) | Variable::Constant(_) => 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()
.as_str()
.parse::<std::net::IpAddr>()
.is_ok()
.into()
}
pub(crate) fn fn_is_ipv4_addr(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| matches!(ip, IpAddr::V4(_)))
.into()
}
pub(crate) fn fn_is_ipv6_addr(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| matches!(ip, IpAddr::V6(_)))
.into()
}
pub(crate) fn fn_is_ip_in_cidr(v: Vec<Variable>) -> Variable {
let Ok(ip) = v[0].to_string().as_str().parse::<IpAddr>() else {
return false.into();
};
IpAddrOrMask::from_str(v[1].to_string().as_str())
.map(|mask| mask.matches(&ip))
.unwrap_or(false)
.into()
}
pub(crate) fn fn_ip_reverse_name(v: Vec<Variable>) -> Variable {
CompactString::new(
v[0].to_string()
.as_str()
.parse::<std::net::IpAddr>()
.map(|ip| ip.to_reverse_name())
.unwrap_or_default(),
)
.into()
}
pub(crate) fn fn_if_then(v: Vec<Variable>) -> Variable {
let mut v = v.into_iter();
let condition = v.next().unwrap();
let iff = v.next().unwrap();
let then = v.next().unwrap();
if condition.to_bool() { iff } else { then }
}
+118
View File
@@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{StringCow, Variable};
use registry::schema::enums::ExpressionVariable;
pub mod array;
pub mod asynch;
pub mod email;
pub mod misc;
pub mod text;
pub trait ResolveVariable: Sync + Send {
fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_>;
fn resolve_global(&self, variable: &str) -> Variable<'_>;
}
impl<'x> Variable<'x> {
fn transform(self, f: impl Fn(StringCow<'x>) -> 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),
("is_ip_in_cidr", misc::fn_is_ip_in_cidr, 2),
("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_n", text::fn_split_n, 3),
("split_words", text::fn_split_words, 1),
("hash", text::fn_hash, 2),
("if_then", misc::fn_if_then, 3),
];
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, 1),
("is_local_address", F_IS_LOCAL_ADDRESS, 1),
("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),
];
pub struct EmptyResolver;
impl ResolveVariable for EmptyResolver {
fn resolve_variable(&self, _: ExpressionVariable) -> Variable<'_> {
Variable::Integer(0)
}
fn resolve_global(&self, _: &str) -> Variable<'_> {
Variable::Integer(0)
}
}
+359
View File
@@ -0,0 +1,359 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString, format_compact};
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use utils::HexEncode;
use crate::expr::{StringCow, Variable};
pub(crate) fn fn_trim(mut v: Vec<Variable>) -> Variable {
v.remove(0).transform(|s| match s {
StringCow::Borrowed(s) => Variable::from(s.trim()),
StringCow::Owned(s) => Variable::from(s.trim().to_compact_string()),
})
}
pub(crate) fn fn_trim_end(mut v: Vec<Variable>) -> Variable {
v.remove(0).transform(|s| match s {
StringCow::Borrowed(s) => Variable::from(s.trim_end()),
StringCow::Owned(s) => Variable::from(s.trim_end().to_compact_string()),
})
}
pub(crate) fn fn_trim_start(mut v: Vec<Variable>) -> Variable {
v.remove(0).transform(|s| match s {
StringCow::Borrowed(s) => Variable::from(s.trim_start()),
StringCow::Owned(s) => Variable::from(s.trim_start().to_compact_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(CompactString::from_str_to_lowercase(s.as_str())))
}
pub(crate) fn fn_to_uppercase(mut v: Vec<Variable>) -> Variable {
v.remove(0)
.transform(|s| Variable::from(CompactString::from_str_to_uppercase(s.as_str())))
}
pub(crate) fn fn_is_uppercase(mut v: Vec<Variable>) -> Variable {
v.remove(0).transform(|s| {
s.as_str()
.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.as_str()
.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.as_str().chars().any(|c| c.is_ascii_digit()).into())
}
pub(crate) fn fn_split_words(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.split_whitespace()
.filter(|word| word.chars().all(|c| c.is_alphanumeric()))
.map(|word| Variable::from(CompactString::new(word)))
.collect::<Vec<_>>()
.into()
}
pub(crate) fn fn_count_spaces(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.chars()
.filter(|c| c.is_whitespace())
.count()
.into()
}
pub(crate) fn fn_count_uppercase(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.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_str()
.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_str().chars().count().into()
}
pub(crate) fn fn_eq_ignore_case(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.eq_ignore_ascii_case(v[1].to_string().as_str())
.into()
}
pub(crate) fn fn_contains(v: Vec<Variable>) -> Variable {
match &v[0] {
Variable::String(s) => s.as_str().contains(v[1].to_string().as_str()),
Variable::Array(arr) => arr.contains(&v[1]),
val => val.to_string().as_str().contains(v[1].to_string().as_str()),
}
.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
.as_str()
.to_lowercase()
.contains(&needle.as_str().to_lowercase()),
Variable::Array(arr) => arr.iter().any(|v| match v {
Variable::String(s) => s.as_str().eq_ignore_ascii_case(needle.as_str()),
_ => false,
}),
val => val.to_string().as_str().contains(needle.as_str()),
}
.into()
}
pub(crate) fn fn_starts_with(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.starts_with(v[1].to_string().as_str())
.into()
}
pub(crate) fn fn_ends_with(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.ends_with(v[1].to_string().as_str())
.into()
}
pub(crate) fn fn_lines(mut v: Vec<Variable>) -> Variable {
match v.remove(0) {
Variable::String(s) => s
.as_str()
.lines()
.map(|s| Variable::from(CompactString::new(s)))
.collect::<Vec<_>>()
.into(),
val => val,
}
}
pub(crate) fn fn_substring(v: Vec<Variable>) -> Variable {
v[0].to_string()
.as_str()
.chars()
.skip(v[1].to_usize().unwrap_or_default())
.take(v[2].to_usize().unwrap_or_default())
.collect::<CompactString>()
.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 {
StringCow::Borrowed(s) => s
.strip_prefix(prefix.as_str())
.map(Variable::from)
.unwrap_or_default(),
StringCow::Owned(s) => s
.strip_prefix(prefix.as_str())
.map(|s| Variable::from(CompactString::new(s)))
.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 {
StringCow::Borrowed(s) => s
.strip_suffix(suffix.as_str())
.map(Variable::from)
.unwrap_or_default(),
StringCow::Owned(s) => s
.strip_suffix(suffix.as_str())
.map(|s| Variable::from(CompactString::new(s)))
.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 {
StringCow::Borrowed(s) => s
.split(arg.as_str())
.map(Variable::from)
.collect::<Vec<_>>()
.into(),
StringCow::Owned(s) => s
.split(arg.as_str())
.map(|s| Variable::from(CompactString::new(s)))
.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 {
StringCow::Borrowed(s) => s
.rsplit(arg.as_str())
.map(Variable::from)
.collect::<Vec<_>>()
.into(),
StringCow::Owned(s) => s
.rsplit(arg.as_str())
.map(|s| Variable::from(CompactString::new(s)))
.collect::<Vec<_>>()
.into(),
}
}
pub(crate) fn fn_split_n(v: Vec<Variable>) -> Variable {
let mut v = v.into_iter();
let value = v.next().unwrap().into_string();
let arg = v.next().unwrap().into_string();
let num = v.next().unwrap().to_integer().unwrap_or_default() as usize;
fn split_n<'x, 'y>(s: &'x str, arg: &'y str, num: usize, mut f: impl FnMut(&'x str)) {
let mut s = s;
for _ in 0..num {
if let Some((a, b)) = s.split_once(arg) {
f(a);
s = b;
} else {
break;
}
}
f(s);
}
let mut result = Vec::new();
match value {
StringCow::Borrowed(s) => split_n(s, arg.as_str(), num, |s| result.push(Variable::from(s))),
StringCow::Owned(s) => split_n(&s, arg.as_str(), num, |s| {
result.push(Variable::from(CompactString::new(s)))
}),
}
result.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 {
StringCow::Borrowed(s) => s
.split_once(arg.as_str())
.map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)]))
.unwrap_or_default(),
StringCow::Owned(s) => s
.split_once(arg.as_str())
.map(|(a, b)| {
Variable::Array(vec![
Variable::from(CompactString::new(a)),
Variable::from(CompactString::new(b)),
])
})
.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 {
StringCow::Borrowed(s) => s
.rsplit_once(arg.as_str())
.map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)]))
.unwrap_or_default(),
StringCow::Owned(s) => s
.rsplit_once(arg.as_str())
.map(|(a, b)| {
Variable::Array(vec![
Variable::from(CompactString::new(a)),
Variable::from(CompactString::new(b)),
])
})
.unwrap_or_default(),
}
}
pub(crate) fn fn_hash(v: Vec<Variable>) -> Variable {
use sha1::Digest;
let mut v = v.into_iter();
let value = v.next().unwrap().into_string();
let algo = v.next().unwrap().into_string();
match algo.as_str() {
"md5" => format_compact!("{:x}", md5::compute(value.as_bytes())).into(),
"sha1" => {
let mut hasher = Sha1::new();
hasher.update(value.as_bytes());
hasher.finalize().hex_encode().to_compact_string().into()
}
"sha256" => {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
hasher.finalize().hex_encode().to_compact_string().into()
}
"sha512" => {
let mut hasher = Sha512::new();
hasher.update(value.as_bytes());
hasher.finalize().hex_encode().to_compact_string().into()
}
_ => Variable::default(),
}
}