You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meli/melib/src/sieve/compiler/grammar/mod.rs

689 lines
23 KiB
Rust

/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart Sieve Interpreter.
*
* 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::fmt::Display;
use phf::phf_map;
use serde::{Deserialize, Serialize};
use self::instruction::CompilerState;
use super::{
lexer::{tokenizer::TokenInfo, word::Word, Token},
CompileError, ErrorType, Regex, Value,
};
pub mod actions;
pub mod expr;
pub mod instruction;
pub mod test;
pub mod tests;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum Capability {
Envelope,
EnvelopeDsn,
EnvelopeDeliverBy,
FileInto,
EncodedCharacter,
Comparator(Comparator),
Other(String),
Body,
Convert,
Copy,
Relational,
Date,
Index,
Duplicate,
Variables,
EditHeader,
ForEveryPart,
Mime,
Replace,
Enclose,
ExtractText,
Enotify,
RedirectDsn,
RedirectDeliverBy,
Environment,
Reject,
Ereject,
ExtLists,
SubAddress,
Vacation,
VacationSeconds,
Fcc,
Mailbox,
MailboxId,
MboxMetadata,
ServerMetadata,
SpecialUse,
Imap4Flags,
Ihave,
ImapSieve,
Include,
Regex,
SpamTest,
SpamTestPlus,
VirusTest,
// Extensions
Eval,
Plugins,
ForEveryLine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AddressPart {
LocalPart,
Domain,
All,
User,
Detail,
Name,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum MatchType {
Is,
Contains,
Matches(u64),
Regex(u64),
Value(RelationalMatch),
Count(RelationalMatch),
List,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum RelationalMatch {
Gt,
Ge,
Lt,
Le,
Eq,
Ne,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum Comparator {
Elbonia,
Octet,
AsciiCaseMap,
AsciiNumeric,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Clear {
pub(crate) local_vars_idx: u32,
pub(crate) local_vars_num: u32,
pub(crate) match_vars: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Invalid {
pub(crate) name: String,
pub(crate) line_num: usize,
pub(crate) line_pos: usize,
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub(crate) struct ForEveryLine {
pub var_idx: usize,
pub jz_pos: usize,
}
impl<'x> CompilerState<'x> {
#[inline(always)]
pub fn expect_instruction_end(&mut self) -> Result<(), CompileError> {
self.tokens.expect_token(Token::Semicolon)
}
pub fn ignore_instruction(&mut self) -> Result<(), CompileError> {
// Skip entire instruction
let mut curly_count = 0;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Semicolon if curly_count == 0 => {
break;
}
Token::CurlyOpen => {
curly_count += 1;
}
Token::CurlyClose => match curly_count {
0 => {
return Err(token_info.expected("instruction"));
}
1 => {
break;
}
_ => curly_count -= 1,
},
_ => (),
}
}
Ok(())
}
pub fn ignore_test(&mut self) -> Result<(), CompileError> {
let mut d_count = 0;
while let Some(token_info) = self.tokens.peek() {
match token_info?.token {
Token::ParenthesisOpen => {
d_count += 1;
}
Token::ParenthesisClose => {
if d_count == 0 {
break;
} else {
d_count -= 1;
}
}
Token::Comma => {
if d_count == 0 {
break;
}
}
Token::CurlyOpen => {
break;
}
_ => (),
}
self.tokens.next();
}
Ok(())
}
pub fn parse_match_type(&mut self, word: Word) -> Result<MatchType, CompileError> {
match word {
Word::Is => Ok(MatchType::Is),
Word::Contains => Ok(MatchType::Contains),
Word::Matches => {
self.block.match_test_pos.push(self.instructions.len());
Ok(MatchType::Matches(0))
}
Word::Regex => {
self.block.match_test_pos.push(self.instructions.len());
Ok(MatchType::Regex(0))
}
Word::List => Ok(MatchType::List),
_ => {
let token_info = self.tokens.unwrap_next()?;
if let Token::StringConstant(text) = &token_info.token {
if let Some(relational) = RELATIONAL.get(text.to_string().as_ref()) {
return Ok(if word == Word::Value {
MatchType::Value(*relational)
} else {
MatchType::Count(*relational)
});
}
}
Err(token_info.expected("relational match"))
}
}
}
pub(crate) fn parse_comparator(&mut self) -> Result<Comparator, CompileError> {
let comparator = self.tokens.expect_static_string()?;
Ok(if let Some(comparator) = COMPARATOR.get(&comparator) {
comparator.clone()
} else {
Comparator::Other(comparator)
})
}
pub(crate) fn parse_static_strings(&mut self) -> Result<Vec<String>, CompileError> {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::BracketOpen => {
let mut strings = Vec::new();
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::StringConstant(string) => {
strings.push(string.into_string());
}
Token::Comma => (),
Token::BracketClose if !strings.is_empty() => break,
_ => return Err(token_info.expected("constant string")),
}
}
Ok(strings)
}
Token::StringConstant(string) => Ok(vec![string.into_string()]),
_ => Err(token_info.expected("'[' or constant string")),
}
}
pub fn parse_string(&mut self) -> Result<Value, CompileError> {
let next_token = self.tokens.unwrap_next()?;
match next_token.token {
Token::StringConstant(s) => Ok(Value::from(s)),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map_err(|error_type| CompileError {
line_num: next_token.line_num,
line_pos: next_token.line_pos,
error_type,
})
}
Token::BracketOpen => {
let mut items = self.parse_string_list(false)?;
match items.pop() {
Some(s) if items.is_empty() => Ok(s),
_ => Err(next_token.expected("string")),
}
}
_ => Err(next_token.expected("string")),
}
}
pub(crate) fn parse_strings(&mut self, allow_empty: bool) -> Result<Vec<Value>, CompileError> {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::BracketOpen => self.parse_string_list(allow_empty),
Token::StringConstant(s) => Ok(vec![Value::from(s)]),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map(|s| vec![s])
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
})
}
_ => Err(token_info.expected("'[' or string")),
}
}
pub(crate) fn parse_string_token(
&mut self,
token_info: TokenInfo,
) -> Result<Value, CompileError> {
match token_info.token {
Token::StringConstant(s) => Ok(Value::from(s)),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
})
}
_ => Err(token_info.expected("string")),
}
}
pub(crate) fn parse_strings_token(
&mut self,
token_info: TokenInfo,
) -> Result<Vec<Value>, CompileError> {
match token_info.token {
Token::StringConstant(s) => Ok(vec![Value::from(s)]),
Token::StringVariable(s) => {
self.tokenize_string(&s, true)
.map(|s| vec![s])
.map_err(|error_type| CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
})
}
Token::BracketOpen => self.parse_string_list(false),
_ => Err(token_info.expected("string")),
}
}
pub(crate) fn parse_string_list(
&mut self,
allow_empty: bool,
) -> Result<Vec<Value>, CompileError> {
let mut strings = Vec::new();
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::StringConstant(s) => {
strings.push(Value::from(s));
}
Token::StringVariable(s) => {
strings.push(self.tokenize_string(&s, true).map_err(|error_type| {
CompileError {
line_num: token_info.line_num,
line_pos: token_info.line_pos,
error_type,
}
})?);
}
Token::Comma => (),
Token::BracketClose if !strings.is_empty() || allow_empty => break,
_ => return Err(token_info.expected("string or string list")),
}
}
Ok(strings)
}
#[inline(always)]
pub(crate) fn has_capability(&self, capability: &Capability) -> bool {
[&self.block]
.into_iter()
.chain(self.block_stack.iter())
.any(|b| b.capabilities.contains(capability))
}
#[inline(always)]
pub(crate) fn reset_param_check(&mut self) {
self.param_check.fill(false);
}
#[inline(always)]
pub(crate) fn validate_argument(
&mut self,
arg_num: usize,
capability: Option<Capability>,
line_num: usize,
line_pos: usize,
) -> Result<(), CompileError> {
if arg_num > 0 {
if let Some(param) = self.param_check.get_mut(arg_num - 1) {
if !*param {
*param = true;
} else {
return Err(CompileError {
line_num,
line_pos,
error_type: ErrorType::DuplicatedParameter,
});
}
} else {
#[cfg(test)]
panic!("Argument out of range {arg_num}");
}
}
if let Some(capability) = capability {
if !self.has_capability(&capability) {
return Err(CompileError {
line_num,
line_pos,
error_type: ErrorType::UndeclaredCapability(capability),
});
}
}
Ok(())
}
pub(crate) fn validate_match(
&mut self,
match_type: &MatchType,
key_list: &mut [Value],
) -> Result<(), CompileError> {
if matches!(match_type, MatchType::Regex(_)) {
for key in key_list {
if let Value::Text(expr) = key {
match fancy_regex::Regex::new(expr) {
Ok(regex) => {
*key = Value::Regex(Regex {
regex,
expr: std::mem::take(expr),
});
}
Err(err) => {
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidRegex(format!("{expr}: {err}"))));
}
}
}
}
}
Ok(())
}
}
impl Capability {
pub fn parse(capability: &str) -> Capability {
if let Some(capability) = CAPABILITIES.get(capability) {
capability.clone()
} else if let Some(comparator) = capability.strip_prefix("comparator-") {
Capability::Comparator(Comparator::Other(comparator.to_string()))
} else {
Capability::Other(capability.to_string())
}
}
pub fn all() -> &'static [Capability] {
&[
Capability::Envelope,
Capability::EnvelopeDsn,
Capability::EnvelopeDeliverBy,
Capability::FileInto,
Capability::EncodedCharacter,
Capability::Comparator(Comparator::Elbonia),
Capability::Comparator(Comparator::AsciiCaseMap),
Capability::Comparator(Comparator::AsciiNumeric),
Capability::Comparator(Comparator::Octet),
Capability::Body,
Capability::Convert,
Capability::Copy,
Capability::Relational,
Capability::Date,
Capability::Index,
Capability::Duplicate,
Capability::Variables,
Capability::EditHeader,
Capability::ForEveryPart,
Capability::Mime,
Capability::Replace,
Capability::Enclose,
Capability::ExtractText,
Capability::Enotify,
Capability::RedirectDsn,
Capability::RedirectDeliverBy,
Capability::Environment,
Capability::Reject,
Capability::Ereject,
Capability::ExtLists,
Capability::SubAddress,
Capability::Vacation,
Capability::VacationSeconds,
Capability::Fcc,
Capability::Mailbox,
Capability::MailboxId,
Capability::MboxMetadata,
Capability::ServerMetadata,
Capability::SpecialUse,
Capability::Imap4Flags,
Capability::Ihave,
Capability::ImapSieve,
Capability::Include,
Capability::Regex,
Capability::SpamTest,
Capability::SpamTestPlus,
Capability::VirusTest,
]
}
}
static RELATIONAL: phf::Map<&'static str, RelationalMatch> = phf_map! {
"gt" => RelationalMatch::Gt,
"ge" => RelationalMatch::Ge,
"lt" => RelationalMatch::Lt,
"le" => RelationalMatch::Le,
"eq" => RelationalMatch::Eq,
"ne" => RelationalMatch::Ne,
};
static COMPARATOR: phf::Map<&'static str, Comparator> = phf_map! {
"i;octet" => Comparator::Octet,
"i;ascii-casemap" => Comparator::AsciiCaseMap,
"i;ascii-numeric" => Comparator::AsciiNumeric,
};
impl Invalid {
pub fn name(&self) -> &str {
&self.name
}
pub fn line_num(&self) -> usize {
self.line_num
}
pub fn line_pos(&self) -> usize {
self.line_pos
}
}
impl From<&str> for Capability {
fn from(value: &str) -> Self {
Capability::parse(value)
}
}
impl From<String> for Capability {
fn from(value: String) -> Self {
Capability::parse(&value)
}
}
impl Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Capability::Envelope => f.write_str("envelope"),
Capability::EnvelopeDsn => f.write_str("envelope-dsn"),
Capability::EnvelopeDeliverBy => f.write_str("envelope-deliverby"),
Capability::FileInto => f.write_str("fileinto"),
Capability::EncodedCharacter => f.write_str("encoded-character"),
Capability::Comparator(Comparator::Elbonia) => f.write_str("comparator-elbonia"),
Capability::Comparator(Comparator::Octet) => f.write_str("comparator-i;octet"),
Capability::Comparator(Comparator::AsciiCaseMap) => {
f.write_str("comparator-i;ascii-casemap")
}
Capability::Comparator(Comparator::AsciiNumeric) => {
f.write_str("comparator-i;ascii-numeric")
}
Capability::Comparator(Comparator::Other(comparator)) => f.write_str(comparator),
Capability::Body => f.write_str("body"),
Capability::Convert => f.write_str("convert"),
Capability::Copy => f.write_str("copy"),
Capability::Relational => f.write_str("relational"),
Capability::Date => f.write_str("date"),
Capability::Index => f.write_str("index"),
Capability::Duplicate => f.write_str("duplicate"),
Capability::Variables => f.write_str("variables"),
Capability::EditHeader => f.write_str("editheader"),
Capability::ForEveryPart => f.write_str("foreverypart"),
Capability::Mime => f.write_str("mime"),
Capability::Replace => f.write_str("replace"),
Capability::Enclose => f.write_str("enclose"),
Capability::ExtractText => f.write_str("extracttext"),
Capability::Enotify => f.write_str("enotify"),
Capability::RedirectDsn => f.write_str("redirect-dsn"),
Capability::RedirectDeliverBy => f.write_str("redirect-deliverby"),
Capability::Environment => f.write_str("environment"),
Capability::Reject => f.write_str("reject"),
Capability::Ereject => f.write_str("ereject"),
Capability::ExtLists => f.write_str("extlists"),
Capability::SubAddress => f.write_str("subaddress"),
Capability::Vacation => f.write_str("vacation"),
Capability::VacationSeconds => f.write_str("vacation-seconds"),
Capability::Fcc => f.write_str("fcc"),
Capability::Mailbox => f.write_str("mailbox"),
Capability::MailboxId => f.write_str("mailboxid"),
Capability::MboxMetadata => f.write_str("mboxmetadata"),
Capability::ServerMetadata => f.write_str("servermetadata"),
Capability::SpecialUse => f.write_str("special-use"),
Capability::Imap4Flags => f.write_str("imap4flags"),
Capability::Ihave => f.write_str("ihave"),
Capability::ImapSieve => f.write_str("imapsieve"),
Capability::Include => f.write_str("include"),
Capability::Regex => f.write_str("regex"),
Capability::SpamTest => f.write_str("spamtest"),
Capability::SpamTestPlus => f.write_str("spamtestplus"),
Capability::VirusTest => f.write_str("virustest"),
Capability::Plugins => f.write_str("vnd.stalwart.plugins"),
Capability::ForEveryLine => f.write_str("vnd.stalwart.foreveryline"),
Capability::Eval => f.write_str("vnd.stalwart.eval"),
Capability::Other(capability) => f.write_str(capability),
}
}
}
static CAPABILITIES: phf::Map<&'static str, Capability> = phf_map! {
"envelope" => Capability::Envelope,
"envelope-dsn" => Capability::EnvelopeDsn,
"envelope-deliverby" => Capability::EnvelopeDeliverBy,
"fileinto" => Capability::FileInto,
"encoded-character" => Capability::EncodedCharacter,
"comparator-elbonia" => Capability::Comparator(Comparator::Elbonia),
"comparator-i;octet" => Capability::Comparator(Comparator::Octet),
"comparator-i;ascii-casemap" => Capability::Comparator(Comparator::AsciiCaseMap),
"comparator-i;ascii-numeric" => Capability::Comparator(Comparator::AsciiNumeric),
"body" => Capability::Body,
"convert" => Capability::Convert,
"copy" => Capability::Copy,
"relational" => Capability::Relational,
"date" => Capability::Date,
"index" => Capability::Index,
"duplicate" => Capability::Duplicate,
"variables" => Capability::Variables,
"editheader" => Capability::EditHeader,
"foreverypart" => Capability::ForEveryPart,
"mime" => Capability::Mime,
"replace" => Capability::Replace,
"enclose" => Capability::Enclose,
"extracttext" => Capability::ExtractText,
"enotify" => Capability::Enotify,
"redirect-dsn" => Capability::RedirectDsn,
"redirect-deliverby" => Capability::RedirectDeliverBy,
"environment" => Capability::Environment,
"reject" => Capability::Reject,
"ereject" => Capability::Ereject,
"extlists" => Capability::ExtLists,
"subaddress" => Capability::SubAddress,
"vacation" => Capability::Vacation,
"vacation-seconds" => Capability::VacationSeconds,
"fcc" => Capability::Fcc,
"mailbox" => Capability::Mailbox,
"mailboxid" => Capability::MailboxId,
"mboxmetadata" => Capability::MboxMetadata,
"servermetadata" => Capability::ServerMetadata,
"special-use" => Capability::SpecialUse,
"imap4flags" => Capability::Imap4Flags,
"ihave" => Capability::Ihave,
"imapsieve" => Capability::ImapSieve,
"include" => Capability::Include,
"regex" => Capability::Regex,
"spamtest" => Capability::SpamTest,
"spamtestplus" => Capability::SpamTestPlus,
"virustest" => Capability::VirusTest,
// Extensions
"vnd.stalwart.plugins" => Capability::Plugins,
"vnd.stalwart.foreveryline" => Capability::ForEveryLine,
"vnd.stalwart.eval" => Capability::Eval,
};