mirror of https://git.meli.delivery/meli/meli
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.
1204 lines
48 KiB
Rust
1204 lines
48 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 ahash::{AHashMap, AHashSet};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::sieve::{
|
|
compiler::{
|
|
grammar::{test::Test, MatchType},
|
|
lexer::{tokenizer::Tokenizer, word::Word, Token},
|
|
CompileError, ErrorType, Value, VariableType,
|
|
},
|
|
Compiler, Sieve,
|
|
};
|
|
|
|
use super::{
|
|
actions::{
|
|
action_convert::Convert,
|
|
action_editheader::{AddHeader, DeleteHeader},
|
|
action_fileinto::FileInto,
|
|
action_flags::EditFlags,
|
|
action_include::Include,
|
|
action_keep::Keep,
|
|
action_mime::{Enclose, ExtractText, ForEveryPart, Replace},
|
|
action_notify::Notify,
|
|
action_redirect::Redirect,
|
|
action_reject::Reject,
|
|
action_set::Set,
|
|
action_vacation::Vacation,
|
|
},
|
|
expr::Expression,
|
|
tests::test_plugin::Plugin,
|
|
Capability, Clear, ForEveryLine, Invalid,
|
|
};
|
|
|
|
use super::tests::test_ihave::Error;
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
|
pub(crate) enum Instruction {
|
|
Require(Vec<Capability>),
|
|
Keep(Keep),
|
|
FileInto(FileInto),
|
|
Redirect(Redirect),
|
|
Discard,
|
|
Stop,
|
|
Invalid(Invalid),
|
|
Test(Test),
|
|
Jmp(usize),
|
|
Jz(usize),
|
|
Jnz(usize),
|
|
|
|
// RFC 5703
|
|
ForEveryPartPush,
|
|
ForEveryPart(ForEveryPart),
|
|
ForEveryPartPop(usize),
|
|
Replace(Replace),
|
|
Enclose(Enclose),
|
|
ExtractText(ExtractText),
|
|
|
|
// RFC 6558
|
|
Convert(Convert),
|
|
|
|
// RFC 5293
|
|
AddHeader(AddHeader),
|
|
DeleteHeader(DeleteHeader),
|
|
|
|
// RFC 5229
|
|
Set(Set),
|
|
Clear(Clear),
|
|
|
|
// RFC 5435
|
|
Notify(Notify),
|
|
|
|
// RFC 5429
|
|
Reject(Reject),
|
|
|
|
// RFC 5230
|
|
Vacation(Vacation),
|
|
|
|
// RFC 5463
|
|
Error(Error),
|
|
|
|
// RFC 5232
|
|
EditFlags(EditFlags),
|
|
|
|
// RFC 6609
|
|
Include(Include),
|
|
Return,
|
|
|
|
// Plugin extension
|
|
Plugin(Plugin),
|
|
|
|
// For every line extension
|
|
ForEveryLineInit(Value),
|
|
ForEveryLine(ForEveryLine),
|
|
}
|
|
|
|
pub(crate) const MAX_PARAMS: usize = 11;
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct Block {
|
|
pub(crate) btype: Word,
|
|
pub(crate) label: Option<String>,
|
|
pub(crate) line_num: usize,
|
|
pub(crate) line_pos: usize,
|
|
pub(crate) last_block_start: usize,
|
|
pub(crate) if_jmps: Vec<usize>,
|
|
pub(crate) break_jmps: Vec<usize>,
|
|
pub(crate) match_test_pos: Vec<usize>,
|
|
pub(crate) match_test_vars: u64,
|
|
pub(crate) vars_local: AHashMap<String, usize>,
|
|
pub(crate) capabilities: AHashSet<Capability>,
|
|
pub(crate) require_pos: usize,
|
|
}
|
|
|
|
pub(crate) struct CompilerState<'x> {
|
|
pub(crate) compiler: &'x Compiler,
|
|
pub(crate) tokens: Tokenizer<'x>,
|
|
pub(crate) instructions: Vec<Instruction>,
|
|
pub(crate) block_stack: Vec<Block>,
|
|
pub(crate) block: Block,
|
|
pub(crate) last_block_type: Word,
|
|
pub(crate) vars_global: AHashSet<String>,
|
|
pub(crate) vars_num: usize,
|
|
pub(crate) vars_num_max: usize,
|
|
pub(crate) vars_match_max: usize,
|
|
pub(crate) vars_local: usize,
|
|
pub(crate) param_check: [bool; MAX_PARAMS],
|
|
pub(crate) includes_num: usize,
|
|
}
|
|
|
|
impl Compiler {
|
|
pub fn compile(&self, script: &[u8]) -> Result<Sieve, CompileError> {
|
|
if script.len() > self.max_script_size {
|
|
return Err(CompileError {
|
|
line_num: 0,
|
|
line_pos: 0,
|
|
error_type: ErrorType::ScriptTooLong,
|
|
});
|
|
}
|
|
|
|
let mut state = CompilerState {
|
|
compiler: self,
|
|
tokens: Tokenizer::new(self, script),
|
|
instructions: Vec::new(),
|
|
block_stack: Vec::new(),
|
|
block: Block::new(Word::Not),
|
|
last_block_type: Word::Not,
|
|
vars_global: AHashSet::new(),
|
|
vars_num: 0,
|
|
vars_num_max: 0,
|
|
vars_match_max: 0,
|
|
vars_local: 0,
|
|
param_check: [false; MAX_PARAMS],
|
|
includes_num: 0,
|
|
};
|
|
|
|
while let Some(token_info) = state.tokens.next() {
|
|
let token_info = token_info?;
|
|
state.reset_param_check();
|
|
|
|
match token_info.token {
|
|
Token::Identifier(instruction) => {
|
|
let mut is_new_block = None;
|
|
|
|
match instruction {
|
|
Word::Require => {
|
|
state.parse_require()?;
|
|
}
|
|
Word::If => {
|
|
state.parse_test()?;
|
|
state.block.if_jmps.clear();
|
|
is_new_block = Block::new(Word::If).into();
|
|
}
|
|
Word::ElsIf => {
|
|
if let Word::If | Word::ElsIf = &state.last_block_type {
|
|
state.parse_test()?;
|
|
is_new_block = Block::new(Word::ElsIf).into();
|
|
} else {
|
|
return Err(token_info.expected("'if' before 'elsif'"));
|
|
}
|
|
}
|
|
Word::Else => {
|
|
if let Word::If | Word::ElsIf = &state.last_block_type {
|
|
is_new_block = Block::new(Word::Else).into();
|
|
} else {
|
|
return Err(token_info.expected("'if' or 'elsif' before 'else'"));
|
|
}
|
|
}
|
|
Word::Keep => {
|
|
state.parse_keep()?;
|
|
}
|
|
Word::FileInto => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::FileInto.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_fileinto()?;
|
|
}
|
|
Word::Redirect => {
|
|
state.parse_redirect()?;
|
|
}
|
|
Word::Discard => {
|
|
state.instructions.push(Instruction::Discard);
|
|
}
|
|
Word::Stop => {
|
|
state.instructions.push(Instruction::Stop);
|
|
}
|
|
|
|
// RFC 5703
|
|
Word::ForEveryPart => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::ForEveryPart.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
|
|
if state
|
|
.block_stack
|
|
.iter()
|
|
.filter(|b| matches!(&b.btype, Word::ForEveryPart))
|
|
.count()
|
|
== self.max_nested_foreverypart
|
|
{
|
|
return Err(
|
|
token_info.custom(ErrorType::TooManyNestedForEveryParts)
|
|
);
|
|
}
|
|
|
|
is_new_block = if let Some(Ok(Token::Tag(Word::Name))) =
|
|
state.tokens.peek().map(|r| r.map(|t| &t.token))
|
|
{
|
|
let tag = state.tokens.next().unwrap().unwrap();
|
|
let label = state.tokens.expect_static_string()?;
|
|
for block in &state.block_stack {
|
|
if block.label.as_ref().map_or(false, |n| n.eq(&label)) {
|
|
return Err(
|
|
tag.custom(ErrorType::LabelAlreadyDefined(label))
|
|
);
|
|
}
|
|
}
|
|
Block::new(Word::ForEveryPart).with_label(label)
|
|
} else {
|
|
Block::new(Word::ForEveryPart)
|
|
}
|
|
.into();
|
|
|
|
state.instructions.push(Instruction::ForEveryPartPush);
|
|
state
|
|
.instructions
|
|
.push(Instruction::ForEveryPart(ForEveryPart {
|
|
jz_pos: usize::MAX,
|
|
}));
|
|
}
|
|
Word::Break => {
|
|
if let Some(Ok(Token::Tag(Word::Name))) =
|
|
state.tokens.peek().map(|r| r.map(|t| &t.token))
|
|
{
|
|
state.validate_argument(
|
|
0,
|
|
Capability::ForEveryPart.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
|
|
let tag = state.tokens.next().unwrap().unwrap();
|
|
let label = state.tokens.expect_static_string()?;
|
|
let mut label_found = false;
|
|
let mut num_pops = 0;
|
|
|
|
for block in [&mut state.block]
|
|
.into_iter()
|
|
.chain(state.block_stack.iter_mut().rev())
|
|
{
|
|
if let Word::ForEveryPart = &block.btype {
|
|
num_pops += 1;
|
|
if block.label.as_ref().map_or(false, |n| n.eq(&label)) {
|
|
state
|
|
.instructions
|
|
.push(Instruction::ForEveryPartPop(num_pops));
|
|
block.break_jmps.push(state.instructions.len());
|
|
label_found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !label_found {
|
|
return Err(tag.custom(ErrorType::LabelUndefined(label)));
|
|
}
|
|
} else {
|
|
let mut block_found = None;
|
|
if matches!(
|
|
&state.block.btype,
|
|
Word::ForEveryPart | Word::ForEveryLine
|
|
) {
|
|
block_found = Some(&mut state.block);
|
|
} else {
|
|
for block in state.block_stack.iter_mut().rev() {
|
|
if matches!(
|
|
&block.btype,
|
|
Word::ForEveryPart | Word::ForEveryLine
|
|
) {
|
|
block_found = Some(block);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let block = block_found.ok_or_else(|| {
|
|
token_info.custom(ErrorType::BreakOutsideLoop)
|
|
})?;
|
|
if matches!(block.btype, Word::ForEveryPart) {
|
|
state.instructions.push(Instruction::ForEveryPartPop(1));
|
|
}
|
|
|
|
block.break_jmps.push(state.instructions.len());
|
|
}
|
|
|
|
state.instructions.push(Instruction::Jmp(usize::MAX));
|
|
}
|
|
Word::Replace => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Replace.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_replace()?;
|
|
}
|
|
Word::Enclose => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Enclose.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_enclose()?;
|
|
}
|
|
Word::ExtractText => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::ExtractText.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_extracttext()?;
|
|
}
|
|
|
|
// RFC 6558
|
|
Word::Convert => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Convert.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_convert()?;
|
|
}
|
|
|
|
// RFC 5293
|
|
Word::AddHeader => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::EditHeader.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_addheader()?;
|
|
}
|
|
Word::DeleteHeader => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::EditHeader.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_deleteheader()?;
|
|
}
|
|
|
|
// RFC 5229
|
|
Word::Set => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Variables.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_set()?;
|
|
}
|
|
|
|
// RFC 5435
|
|
Word::Notify => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Enotify.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_notify()?;
|
|
}
|
|
|
|
// RFC 5429
|
|
Word::Reject => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Reject.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_reject(false)?;
|
|
}
|
|
Word::Ereject => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Ereject.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_reject(true)?;
|
|
}
|
|
|
|
// RFC 5230
|
|
Word::Vacation => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Vacation.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_vacation()?;
|
|
}
|
|
|
|
// RFC 5463
|
|
Word::Error => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Ihave.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_error()?;
|
|
}
|
|
|
|
// RFC 5232
|
|
Word::SetFlag | Word::AddFlag | Word::RemoveFlag => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Imap4Flags.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_flag_action(instruction)?;
|
|
}
|
|
|
|
// RFC 6609
|
|
Word::Include => {
|
|
if state.includes_num < self.max_includes {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Include.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_include()?;
|
|
state.includes_num += 1;
|
|
} else {
|
|
return Err(token_info.custom(ErrorType::TooManyIncludes));
|
|
}
|
|
}
|
|
Word::Return => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Include.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
let mut num_pops = 0;
|
|
|
|
for block in [&state.block]
|
|
.into_iter()
|
|
.chain(state.block_stack.iter().rev())
|
|
{
|
|
if let Word::ForEveryPart = &block.btype {
|
|
num_pops += 1;
|
|
}
|
|
}
|
|
|
|
if num_pops > 0 {
|
|
state
|
|
.instructions
|
|
.push(Instruction::ForEveryPartPop(num_pops));
|
|
}
|
|
|
|
state.instructions.push(Instruction::Return);
|
|
}
|
|
Word::Global => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Include.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Variables.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
for global in state.parse_static_strings()? {
|
|
if !state.is_var_local(&global) {
|
|
if global.len() < self.max_variable_name_size {
|
|
state.register_global_var(&global);
|
|
} else {
|
|
return Err(state
|
|
.tokens
|
|
.unwrap_next()?
|
|
.custom(ErrorType::VariableTooLong));
|
|
}
|
|
} else {
|
|
return Err(state
|
|
.tokens
|
|
.unwrap_next()?
|
|
.custom(ErrorType::VariableIsLocal(global)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ForEveryLine extension
|
|
Word::ForEveryLine => {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::ForEveryLine.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
|
|
if state
|
|
.block_stack
|
|
.iter()
|
|
.any(|b| matches!(&b.btype, Word::ForEveryLine))
|
|
{
|
|
return Err(token_info.custom(ErrorType::TooManyNestedBlocks));
|
|
}
|
|
|
|
let var_idx = state.vars_num;
|
|
is_new_block = Block::new(Word::ForEveryLine)
|
|
.with_local_var("line", state.vars_num)
|
|
.with_local_var("line_num", state.vars_num + 1)
|
|
.into();
|
|
state.vars_num += 2;
|
|
|
|
let source = state.parse_string()?;
|
|
state
|
|
.instructions
|
|
.push(Instruction::ForEveryLineInit(source));
|
|
state
|
|
.instructions
|
|
.push(Instruction::ForEveryLine(ForEveryLine {
|
|
var_idx,
|
|
jz_pos: usize::MAX,
|
|
}));
|
|
}
|
|
|
|
_ => {
|
|
state.ignore_instruction()?;
|
|
state.instructions.push(Instruction::Invalid(Invalid {
|
|
name: instruction.to_string(),
|
|
line_num: token_info.line_num,
|
|
line_pos: token_info.line_pos,
|
|
}));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if let Some(mut new_block) = is_new_block {
|
|
new_block.line_num = state.tokens.line_num;
|
|
new_block.line_pos = state.tokens.pos - state.tokens.line_start;
|
|
|
|
state.tokens.expect_token(Token::CurlyOpen)?;
|
|
if state.block_stack.len() < self.max_nested_blocks {
|
|
state.block.last_block_start = state.instructions.len() - 1;
|
|
state.block_stack.push(state.block);
|
|
state.block = new_block;
|
|
} else {
|
|
return Err(CompileError {
|
|
line_num: state.block.line_num,
|
|
line_pos: state.block.line_pos,
|
|
error_type: ErrorType::TooManyNestedBlocks,
|
|
});
|
|
}
|
|
} else {
|
|
state.expect_instruction_end()?;
|
|
}
|
|
}
|
|
Token::CurlyClose if !state.block_stack.is_empty() => {
|
|
state.block_end();
|
|
let mut prev_block = state.block_stack.pop().unwrap();
|
|
match &state.block.btype {
|
|
Word::ForEveryPart => {
|
|
state
|
|
.instructions
|
|
.push(Instruction::Jmp(prev_block.last_block_start));
|
|
let cur_pos = state.instructions.len();
|
|
if let Instruction::ForEveryPart(fep) =
|
|
&mut state.instructions[prev_block.last_block_start]
|
|
{
|
|
fep.jz_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
for pos in state.block.break_jmps {
|
|
if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] {
|
|
*jmp_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
}
|
|
state.last_block_type = Word::Not;
|
|
}
|
|
Word::If | Word::ElsIf => {
|
|
let next_is_block = matches!(
|
|
state.tokens.peek().map(|r| r.map(|t| &t.token)),
|
|
Some(Ok(Token::Identifier(Word::ElsIf | Word::Else)))
|
|
);
|
|
if next_is_block {
|
|
prev_block.if_jmps.push(state.instructions.len());
|
|
state.instructions.push(Instruction::Jmp(usize::MAX));
|
|
}
|
|
let cur_pos = state.instructions.len();
|
|
if let Instruction::Jz(jmp_pos) =
|
|
&mut state.instructions[prev_block.last_block_start]
|
|
{
|
|
*jmp_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
if !next_is_block {
|
|
for pos in prev_block.if_jmps.drain(..) {
|
|
if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos]
|
|
{
|
|
*jmp_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
}
|
|
state.last_block_type = Word::Not;
|
|
} else {
|
|
state.last_block_type = state.block.btype;
|
|
}
|
|
}
|
|
Word::Else => {
|
|
let cur_pos = state.instructions.len();
|
|
for pos in prev_block.if_jmps.drain(..) {
|
|
if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] {
|
|
*jmp_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
}
|
|
state.last_block_type = Word::Else;
|
|
}
|
|
Word::ForEveryLine => {
|
|
state
|
|
.instructions
|
|
.push(Instruction::Jmp(prev_block.last_block_start));
|
|
let cur_pos = state.instructions.len();
|
|
if let Instruction::ForEveryLine(fep) =
|
|
&mut state.instructions[prev_block.last_block_start]
|
|
{
|
|
fep.jz_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
for pos in state.block.break_jmps {
|
|
if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] {
|
|
*jmp_pos = cur_pos;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
}
|
|
state.last_block_type = Word::Not;
|
|
}
|
|
_ => {
|
|
debug_assert!(false, "This should not have happened.");
|
|
}
|
|
}
|
|
|
|
state.block = prev_block;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
Token::Unknown(instruction) if instruction.contains("test") => {
|
|
use crate::sieve::PluginArgument;
|
|
|
|
let has_arguments = instruction != "test";
|
|
let mut plugin = Plugin {
|
|
id: u32::MAX,
|
|
arguments: vec![PluginArgument::Text(Value::Text(instruction))],
|
|
is_not: false,
|
|
};
|
|
|
|
if !has_arguments {
|
|
plugin
|
|
.arguments
|
|
.push(PluginArgument::Text(state.parse_string()?));
|
|
state.instructions.push(Instruction::Plugin(plugin));
|
|
let mut new_block = Block::new(Word::Else);
|
|
new_block.line_num = state.tokens.line_num;
|
|
new_block.line_pos = state.tokens.pos - state.tokens.line_start;
|
|
state.tokens.expect_token(Token::CurlyOpen)?;
|
|
state.block.last_block_start = state.instructions.len() - 1;
|
|
state.block_stack.push(state.block);
|
|
state.block = new_block;
|
|
} else {
|
|
loop {
|
|
plugin.arguments.push(PluginArgument::Text(
|
|
match state.tokens.unwrap_next()?.token {
|
|
Token::StringConstant(s) => Value::from(s),
|
|
Token::StringVariable(s) => state
|
|
.tokenize_string(&s, true)
|
|
.map_err(|error_type| CompileError {
|
|
line_num: 0,
|
|
line_pos: 0,
|
|
error_type,
|
|
})?,
|
|
Token::Number(n) => Value::Number(
|
|
crate::sieve::compiler::Number::Integer(n as i64),
|
|
),
|
|
Token::Identifier(s) => Value::Text(s.to_string()),
|
|
Token::Tag(s) => Value::Text(format!(":{s}")),
|
|
Token::Unknown(s) => Value::Text(s),
|
|
Token::Semicolon => break,
|
|
other => panic!("Invalid test param {other:?}"),
|
|
},
|
|
));
|
|
}
|
|
state.instructions.push(Instruction::Plugin(plugin));
|
|
}
|
|
}
|
|
|
|
Token::Unknown(instruction) => {
|
|
if let Some(schema) = self.plugins.get(&instruction) {
|
|
state.validate_argument(
|
|
0,
|
|
Capability::Plugins.into(),
|
|
token_info.line_num,
|
|
token_info.line_pos,
|
|
)?;
|
|
state.parse_plugin(schema)?;
|
|
} else {
|
|
state.ignore_instruction()?;
|
|
state.instructions.push(Instruction::Invalid(Invalid {
|
|
name: instruction,
|
|
line_num: token_info.line_num,
|
|
line_pos: token_info.line_pos,
|
|
}));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(token_info.expected("instruction"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !state.block_stack.is_empty() {
|
|
return Err(CompileError {
|
|
line_num: state.block.line_num,
|
|
line_pos: state.block.line_pos,
|
|
error_type: ErrorType::UnterminatedBlock,
|
|
});
|
|
}
|
|
|
|
// Map local variables
|
|
let mut num_vars = std::cmp::max(state.vars_num_max, state.vars_num);
|
|
if state.vars_local > 0 {
|
|
state.map_local_vars(num_vars);
|
|
num_vars += state.vars_local;
|
|
}
|
|
|
|
Ok(Sieve {
|
|
instructions: state.instructions,
|
|
num_vars,
|
|
num_match_vars: state.vars_match_max,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'x> CompilerState<'x> {
|
|
pub(crate) fn is_var_local(&self, name: &str) -> bool {
|
|
let name = name.to_ascii_lowercase();
|
|
if self.block.vars_local.contains_key(&name) {
|
|
true
|
|
} else {
|
|
for block in self.block_stack.iter().rev() {
|
|
if block.vars_local.contains_key(&name) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_var_global(&self, name: &str) -> bool {
|
|
let name = name.to_ascii_lowercase();
|
|
self.vars_global.contains(&name)
|
|
}
|
|
|
|
pub(crate) fn register_local_var(&mut self, name: String, register_as_local: bool) -> usize {
|
|
if let Some(var_id) = self.get_local_var(&name) {
|
|
var_id
|
|
} else if !register_as_local || self.block_stack.is_empty() {
|
|
let var_id = self.vars_num;
|
|
self.block.vars_local.insert(name, var_id);
|
|
self.vars_num += 1;
|
|
var_id
|
|
} else {
|
|
let var_id = usize::MAX - self.vars_local;
|
|
self.block_stack
|
|
.first_mut()
|
|
.unwrap()
|
|
.vars_local
|
|
.insert(name, usize::MAX - self.vars_local);
|
|
self.vars_local += 1;
|
|
var_id
|
|
}
|
|
}
|
|
|
|
pub(crate) fn register_global_var(&mut self, name: &str) {
|
|
self.vars_global.insert(name.to_ascii_lowercase());
|
|
}
|
|
|
|
pub(crate) fn get_local_var(&self, name: &str) -> Option<usize> {
|
|
let name = name.to_ascii_lowercase();
|
|
if let Some(var_id) = self.block.vars_local.get(&name) {
|
|
Some(*var_id)
|
|
} else {
|
|
for block in self.block_stack.iter().rev() {
|
|
if let Some(var_id) = block.vars_local.get(&name) {
|
|
return Some(*var_id);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn register_match_var(&mut self, num: usize) -> bool {
|
|
let mut block = &mut self.block;
|
|
|
|
if block.match_test_pos.is_empty() {
|
|
for block_ in self.block_stack.iter_mut().rev() {
|
|
if !block_.match_test_pos.is_empty() {
|
|
block = block_;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !block.match_test_pos.is_empty() {
|
|
debug_assert!(num < 63);
|
|
|
|
for pos in &block.match_test_pos {
|
|
if let Instruction::Test(test) = &mut self.instructions[*pos] {
|
|
let match_type = match test {
|
|
Test::Address(t) => &mut t.match_type,
|
|
Test::Body(t) => &mut t.match_type,
|
|
Test::Date(t) => &mut t.match_type,
|
|
Test::CurrentDate(t) => &mut t.match_type,
|
|
Test::Envelope(t) => &mut t.match_type,
|
|
Test::HasFlag(t) => &mut t.match_type,
|
|
Test::Header(t) => &mut t.match_type,
|
|
Test::Metadata(t) => &mut t.match_type,
|
|
Test::NotifyMethodCapability(t) => &mut t.match_type,
|
|
Test::SpamTest(t) => &mut t.match_type,
|
|
Test::String(t) | Test::Environment(t) => &mut t.match_type,
|
|
Test::VirusTest(t) => &mut t.match_type,
|
|
_ => {
|
|
debug_assert!(false, "This should not have happened: {test:?}");
|
|
return false;
|
|
}
|
|
};
|
|
if let MatchType::Matches(positions) | MatchType::Regex(positions) = match_type
|
|
{
|
|
*positions |= 1 << num;
|
|
block.match_test_vars = *positions;
|
|
} else {
|
|
debug_assert!(false, "This should not have happened");
|
|
return false;
|
|
}
|
|
} else {
|
|
debug_assert!(false, "This should not have happened");
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub(crate) fn block_end(&mut self) {
|
|
let vars_num_block = self.block.vars_local.len();
|
|
if vars_num_block > 0 {
|
|
if self.vars_num > self.vars_num_max {
|
|
self.vars_num_max = self.vars_num;
|
|
}
|
|
self.vars_num -= vars_num_block;
|
|
self.instructions.push(Instruction::Clear(Clear {
|
|
match_vars: self.block.match_test_vars,
|
|
local_vars_idx: self.vars_num as u32,
|
|
local_vars_num: vars_num_block as u32,
|
|
}));
|
|
} else if self.block.match_test_vars != 0 {
|
|
self.instructions.push(Instruction::Clear(Clear {
|
|
match_vars: self.block.match_test_vars,
|
|
local_vars_idx: 0,
|
|
local_vars_num: 0,
|
|
}));
|
|
}
|
|
}
|
|
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
for instruction in &mut self.instructions {
|
|
match instruction {
|
|
Instruction::Test(v) => v.map_local_vars(last_id),
|
|
Instruction::Keep(k) => k.flags.map_local_vars(last_id),
|
|
Instruction::FileInto(v) => {
|
|
v.folder.map_local_vars(last_id);
|
|
v.flags.map_local_vars(last_id);
|
|
v.mailbox_id.map_local_vars(last_id);
|
|
v.special_use.map_local_vars(last_id);
|
|
}
|
|
Instruction::Redirect(v) => {
|
|
v.address.map_local_vars(last_id);
|
|
v.by_time.map_local_vars(last_id);
|
|
}
|
|
Instruction::Replace(v) => {
|
|
v.subject.map_local_vars(last_id);
|
|
v.from.map_local_vars(last_id);
|
|
v.replacement.map_local_vars(last_id);
|
|
}
|
|
Instruction::Enclose(v) => {
|
|
v.subject.map_local_vars(last_id);
|
|
v.headers.map_local_vars(last_id);
|
|
v.value.map_local_vars(last_id);
|
|
}
|
|
Instruction::ExtractText(v) => {
|
|
v.name.map_local_vars(last_id);
|
|
}
|
|
Instruction::Convert(v) => {
|
|
v.from_media_type.map_local_vars(last_id);
|
|
v.to_media_type.map_local_vars(last_id);
|
|
v.transcoding_params.map_local_vars(last_id);
|
|
}
|
|
Instruction::AddHeader(v) => {
|
|
v.field_name.map_local_vars(last_id);
|
|
v.value.map_local_vars(last_id);
|
|
}
|
|
Instruction::DeleteHeader(v) => {
|
|
v.field_name.map_local_vars(last_id);
|
|
v.value_patterns.map_local_vars(last_id);
|
|
}
|
|
Instruction::Set(v) => {
|
|
v.name.map_local_vars(last_id);
|
|
v.value.map_local_vars(last_id);
|
|
}
|
|
Instruction::Notify(v) => {
|
|
v.from.map_local_vars(last_id);
|
|
v.importance.map_local_vars(last_id);
|
|
v.options.map_local_vars(last_id);
|
|
v.message.map_local_vars(last_id);
|
|
v.fcc.map_local_vars(last_id);
|
|
v.method.map_local_vars(last_id);
|
|
}
|
|
Instruction::Reject(v) => {
|
|
v.reason.map_local_vars(last_id);
|
|
}
|
|
Instruction::Vacation(v) => {
|
|
v.subject.map_local_vars(last_id);
|
|
v.from.map_local_vars(last_id);
|
|
v.fcc.map_local_vars(last_id);
|
|
v.reason.map_local_vars(last_id);
|
|
}
|
|
Instruction::Error(v) => {
|
|
v.message.map_local_vars(last_id);
|
|
}
|
|
Instruction::EditFlags(v) => {
|
|
v.name.map_local_vars(last_id);
|
|
v.flags.map_local_vars(last_id);
|
|
}
|
|
Instruction::Include(v) => {
|
|
v.value.map_local_vars(last_id);
|
|
}
|
|
Instruction::Plugin(v) => {
|
|
v.arguments.map_local_vars(last_id);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait MapLocalVars {
|
|
fn map_local_vars(&mut self, last_id: usize);
|
|
}
|
|
|
|
impl MapLocalVars for Test {
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
match self {
|
|
Test::Address(v) => {
|
|
v.header_list.map_local_vars(last_id);
|
|
v.key_list.map_local_vars(last_id);
|
|
}
|
|
Test::Envelope(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
}
|
|
Test::Exists(v) => {
|
|
v.header_names.map_local_vars(last_id);
|
|
}
|
|
Test::Header(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
v.header_list.map_local_vars(last_id);
|
|
v.mime_opts.map_local_vars(last_id);
|
|
}
|
|
Test::Body(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
}
|
|
Test::Convert(v) => {
|
|
v.from_media_type.map_local_vars(last_id);
|
|
v.to_media_type.map_local_vars(last_id);
|
|
v.transcoding_params.map_local_vars(last_id);
|
|
}
|
|
Test::Date(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
v.header_name.map_local_vars(last_id);
|
|
}
|
|
Test::CurrentDate(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
}
|
|
Test::Duplicate(v) => {
|
|
v.handle.map_local_vars(last_id);
|
|
v.dup_match.map_local_vars(last_id);
|
|
}
|
|
Test::String(v) => {
|
|
v.source.map_local_vars(last_id);
|
|
v.key_list.map_local_vars(last_id);
|
|
}
|
|
Test::Environment(v) => {
|
|
v.source.map_local_vars(last_id);
|
|
v.key_list.map_local_vars(last_id);
|
|
}
|
|
Test::NotifyMethodCapability(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
v.notification_capability.map_local_vars(last_id);
|
|
v.notification_uri.map_local_vars(last_id);
|
|
}
|
|
Test::ValidNotifyMethod(v) => {
|
|
v.notification_uris.map_local_vars(last_id);
|
|
}
|
|
Test::ValidExtList(v) => {
|
|
v.list_names.map_local_vars(last_id);
|
|
}
|
|
Test::HasFlag(v) => {
|
|
v.variable_list.map_local_vars(last_id);
|
|
v.flags.map_local_vars(last_id);
|
|
}
|
|
Test::MailboxExists(v) => {
|
|
v.mailbox_names.map_local_vars(last_id);
|
|
}
|
|
Test::Metadata(v) => {
|
|
v.key_list.map_local_vars(last_id);
|
|
v.medatata.map_local_vars(last_id);
|
|
}
|
|
Test::MetadataExists(v) => {
|
|
v.annotation_names.map_local_vars(last_id);
|
|
v.mailbox.map_local_vars(last_id);
|
|
}
|
|
Test::MailboxIdExists(v) => {
|
|
v.mailbox_ids.map_local_vars(last_id);
|
|
}
|
|
Test::SpamTest(v) => {
|
|
v.value.map_local_vars(last_id);
|
|
}
|
|
Test::VirusTest(v) => {
|
|
v.value.map_local_vars(last_id);
|
|
}
|
|
Test::SpecialUseExists(v) => {
|
|
v.mailbox.map_local_vars(last_id);
|
|
v.attributes.map_local_vars(last_id);
|
|
}
|
|
Test::Vacation(v) => {
|
|
v.addresses.map_local_vars(last_id);
|
|
v.handle.map_local_vars(last_id);
|
|
v.reason.map_local_vars(last_id);
|
|
}
|
|
Test::EvalExpression(v) => {
|
|
v.expr.map_local_vars(last_id);
|
|
}
|
|
Test::Plugin(v) => {
|
|
v.arguments.map_local_vars(last_id);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MapLocalVars for VariableType {
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
match self {
|
|
VariableType::Local(id) if *id > last_id => {
|
|
*id = (usize::MAX - *id) + last_id;
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MapLocalVars for Value {
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
match self {
|
|
Value::Variable(var) => var.map_local_vars(last_id),
|
|
Value::Expression(expr) => expr.map_local_vars(last_id),
|
|
Value::List(items) => items.map_local_vars(last_id),
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: MapLocalVars> MapLocalVars for Option<T> {
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
if let Some(value) = self {
|
|
value.map_local_vars(last_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MapLocalVars for Expression {
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
if let Expression::Variable(var) = self {
|
|
var.map_local_vars(last_id)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: MapLocalVars> MapLocalVars for Vec<T> {
|
|
fn map_local_vars(&mut self, last_id: usize) {
|
|
for item in self {
|
|
item.map_local_vars(last_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Block {
|
|
pub fn new(btype: Word) -> Self {
|
|
Block {
|
|
btype,
|
|
label: None,
|
|
line_num: 0,
|
|
line_pos: 0,
|
|
last_block_start: 0,
|
|
match_test_pos: vec![],
|
|
match_test_vars: 0,
|
|
if_jmps: vec![],
|
|
break_jmps: vec![],
|
|
vars_local: AHashMap::new(),
|
|
capabilities: AHashSet::new(),
|
|
require_pos: usize::MAX,
|
|
}
|
|
}
|
|
|
|
pub fn with_label(mut self, label: String) -> Self {
|
|
self.label = label.into();
|
|
self
|
|
}
|
|
|
|
pub fn with_local_var(mut self, name: impl Into<String>, id: usize) -> Self {
|
|
self.vars_local.insert(name.into(), id);
|
|
self
|
|
}
|
|
}
|