Rename the identifiers that carried the upstream name
Everything clients, users and operators meet now carries the fork's name, with no aliases (SPEC.md §2.4, changed here from "protocol identifiers stay"): - JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside the fork's own urn:inbuxa:jmap. - WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once. - Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in; a unit test fails if Cargo.lock ever moves past the vendored copy. The trusted runtime now names itself too, rather than answering sieve-rs's default. - The web interface's OAuth client is inbuxa-webui. On every start the old stalwart-webui client is removed and any application naming it is moved over. - The spam filter's blobs are INBUXA_SPAM_*; every start moves any left under the old keys, so a trained model survives. - SQL stores and log files default to inbuxa, in the code and in the schema served to the admin (checksum regenerated). - Settings are INBUXA_* only. A STALWART_* variable that's set where its INBUXA_* one isn't stops the server at startup, naming it. - The version-upgrade messages link docs.inbuxa.org's migration page, and the OpenAPI description, smtp crate metadata and web-push test fixtures lose the name. Kept on purpose, allowlisted with reasons: the OAuth key-derivation contexts (renaming them would end every session and invalidate every sealed client id) and the hashed application prefix. Also fixes a latent start-up failure: ensure_client updated an existing first-party client with a revision of 0, which the registry's assertion never matches, so adding a redirect URI or changing the webmail secret failed start-up. And the principal session test now expects legacyProtocols (C-1, added 2026-09-21), which it had missed. Tested: the server builds without warnings; common's 106 unit tests, including the vendoring check; a new integration test for the two start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{
|
||||
instruction::{CompilerState, Instruction},
|
||||
test::Test,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Convert {
|
||||
pub from_media_type: Value,
|
||||
pub to_media_type: Value,
|
||||
pub transcoding_params: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_convert(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::Convert(Convert {
|
||||
from_media_type: self.parse_string()?,
|
||||
to_media_type: self.parse_string()?,
|
||||
transcoding_params: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_convert(&mut self) -> Result<(), CompileError> {
|
||||
let cmd = Instruction::Convert(Convert {
|
||||
from_media_type: self.parse_string()?,
|
||||
to_media_type: self.parse_string()?,
|
||||
transcoding_params: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
});
|
||||
self.instructions.push(cmd);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Value,
|
||||
grammar::{
|
||||
Capability, Comparator,
|
||||
instruction::{CompilerState, Instruction},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::MatchType;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct AddHeader {
|
||||
pub last: bool,
|
||||
pub field_name: Value,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/*
|
||||
Usage: "deleteheader" [":index" <fieldno: number> [":last"]]
|
||||
[COMPARATOR] [MATCH-TYPE]
|
||||
<field-name: string>
|
||||
[<value-patterns: string-list>]
|
||||
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct DeleteHeader {
|
||||
pub index: Option<i32>,
|
||||
pub comparator: Comparator,
|
||||
pub match_type: MatchType,
|
||||
pub field_name: Value,
|
||||
pub value_patterns: Vec<Value>,
|
||||
pub mime_anychild: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_addheader(&mut self) -> Result<(), CompileError> {
|
||||
let mut field_name = None;
|
||||
let value;
|
||||
let mut last = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Last) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
last = true;
|
||||
}
|
||||
_ => {
|
||||
let string = self.parse_string_token(token_info)?;
|
||||
if field_name.is_none() {
|
||||
if let Value::Text(header_name) = &string
|
||||
&& HeaderName::parse(header_name.as_ref()).is_none()
|
||||
{
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::InvalidHeaderName));
|
||||
}
|
||||
|
||||
field_name = string.into();
|
||||
} else {
|
||||
if matches!(
|
||||
&string,
|
||||
Value::Text(value) if value.len() > self.compiler.max_header_size
|
||||
) {
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::HeaderTooLong));
|
||||
}
|
||||
value = string;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.instructions.push(Instruction::AddHeader(AddHeader {
|
||||
last,
|
||||
field_name: field_name.unwrap(),
|
||||
value,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_deleteheader(&mut self) -> Result<(), CompileError> {
|
||||
let field_name: Value;
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut index = None;
|
||||
let mut index_last = false;
|
||||
let mut mime = false;
|
||||
let mut mime_anychild = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Index) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
|
||||
}
|
||||
Token::Tag(Word::Last) => {
|
||||
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
|
||||
index_last = true;
|
||||
}
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::AnyChild) => {
|
||||
self.validate_argument(
|
||||
6,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime_anychild = true;
|
||||
}
|
||||
_ => {
|
||||
field_name = self.parse_string_token(token_info)?;
|
||||
if let Value::Text(header_name) = &field_name
|
||||
&& HeaderName::parse(header_name.as_ref()).is_none()
|
||||
{
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::InvalidHeaderName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mime && mime_anychild {
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
|
||||
}
|
||||
|
||||
let cmd = Instruction::DeleteHeader(DeleteHeader {
|
||||
index: if index_last { index.map(|i| -i) } else { index },
|
||||
comparator,
|
||||
match_type,
|
||||
field_name,
|
||||
value_patterns: if let Some(Ok(
|
||||
Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
|
||||
)) = self.tokens.peek().map(|r| r.map(|t| &t.token))
|
||||
{
|
||||
let mut key_list = self.parse_strings(false)?;
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
key_list
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
mime_anychild,
|
||||
});
|
||||
self.instructions.push(cmd);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{
|
||||
Capability,
|
||||
instruction::{CompilerState, Instruction},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct FileInto {
|
||||
pub copy: bool,
|
||||
pub create: bool,
|
||||
pub folder: Value,
|
||||
pub flags: Vec<Value>,
|
||||
pub mailbox_id: Option<Value>,
|
||||
pub special_use: Option<Value>,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_fileinto(&mut self) -> Result<(), CompileError> {
|
||||
let folder;
|
||||
let mut copy = false;
|
||||
let mut create = false;
|
||||
let mut flags = Vec::new();
|
||||
let mut mailbox_id = None;
|
||||
let mut special_use = None;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Copy) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
Capability::Copy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
copy = true;
|
||||
}
|
||||
Token::Tag(Word::Create) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
Capability::Mailbox.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
create = true;
|
||||
}
|
||||
Token::Tag(Word::Flags) => {
|
||||
self.validate_argument(
|
||||
3,
|
||||
Capability::Imap4Flags.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
flags = self.parse_strings(false)?;
|
||||
}
|
||||
Token::Tag(Word::MailboxId) => {
|
||||
self.validate_argument(
|
||||
4,
|
||||
Capability::Mailbox.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mailbox_id = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::SpecialUse) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::SpecialUse.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
special_use = self.parse_string()?.into();
|
||||
}
|
||||
_ => {
|
||||
folder = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::FileInto(FileInto {
|
||||
folder,
|
||||
copy,
|
||||
create,
|
||||
flags,
|
||||
mailbox_id,
|
||||
special_use,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Value, VariableType,
|
||||
grammar::instruction::{CompilerState, Instruction},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct EditFlags {
|
||||
pub action: Action,
|
||||
pub name: Option<VariableType>,
|
||||
pub flags: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Action {
|
||||
Set,
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_flag_action(&mut self, word: Word) -> Result<(), CompileError> {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
let action = match word {
|
||||
Word::SetFlag => Action::Set,
|
||||
Word::AddFlag => Action::Add,
|
||||
Word::RemoveFlag => Action::Remove,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let instruction = Instruction::EditFlags(
|
||||
match (
|
||||
&token_info.token,
|
||||
self.tokens.peek().map(|r| r.map(|t| &t.token)),
|
||||
) {
|
||||
(
|
||||
Token::StringConstant(_),
|
||||
Some(Ok(
|
||||
Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
|
||||
)),
|
||||
) => EditFlags {
|
||||
name: self.parse_variable_name(token_info, false)?.into(),
|
||||
flags: self.parse_strings(false)?,
|
||||
action,
|
||||
},
|
||||
(Token::BracketOpen, _)
|
||||
| (
|
||||
Token::StringConstant(_) | Token::StringVariable(_),
|
||||
Some(Ok(Token::Semicolon)),
|
||||
) => EditFlags {
|
||||
name: None,
|
||||
flags: self.parse_strings_token(token_info)?,
|
||||
action,
|
||||
},
|
||||
_ => {
|
||||
return Err(token_info.custom(ErrorType::InvalidArguments));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.instructions.push(instruction);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::instruction::{CompilerState, Instruction},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
include [LOCATION] [":once"] [":optional"] <value: string>
|
||||
LOCATION = ":personal" / ":global"
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Include {
|
||||
pub location: Location,
|
||||
pub once: bool,
|
||||
pub optional: bool,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Location {
|
||||
Personal,
|
||||
Global,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_include(&mut self) -> Result<(), CompileError> {
|
||||
let value;
|
||||
let mut once = false;
|
||||
let mut optional = false;
|
||||
let mut location = Location::Personal;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Once) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
once = true;
|
||||
}
|
||||
Token::Tag(Word::Optional) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
optional = true;
|
||||
}
|
||||
Token::Tag(Word::Personal) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
location = Location::Personal;
|
||||
}
|
||||
Token::Tag(Word::Global) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
location = Location::Global;
|
||||
}
|
||||
_ => {
|
||||
value = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::Include(Include {
|
||||
location,
|
||||
once,
|
||||
optional,
|
||||
value,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{
|
||||
Capability,
|
||||
instruction::{CompilerState, Instruction},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Keep {
|
||||
pub flags: Vec<Value>,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_keep(&mut self) -> Result<(), CompileError> {
|
||||
let cmd = Instruction::Keep(Keep {
|
||||
flags: match self.tokens.peek().map(|r| r.map(|t| &t.token)) {
|
||||
Some(Ok(Token::Tag(Word::Flags))) => {
|
||||
let token_info = self.tokens.next().unwrap().unwrap();
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Imap4Flags.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_strings(false)?
|
||||
}
|
||||
_ => Vec::new(),
|
||||
},
|
||||
});
|
||||
self.instructions.push(cmd);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value, VariableType,
|
||||
grammar::instruction::{CompilerState, Instruction},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use super::action_set::Modifier;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct ForEveryPart {
|
||||
pub jz_pos: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Replace {
|
||||
pub subject: Option<Value>,
|
||||
pub from: Option<Value>,
|
||||
pub replacement: Value,
|
||||
pub mime: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Enclose {
|
||||
pub subject: Option<Value>,
|
||||
pub headers: Vec<Value>,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct ExtractText {
|
||||
pub modifiers: Vec<Modifier>,
|
||||
pub first: Option<usize>,
|
||||
pub name: VariableType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum MimeOpts<T> {
|
||||
Type,
|
||||
Subtype,
|
||||
ContentType,
|
||||
Param(Vec<T>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_replace(&mut self) -> Result<(), CompileError> {
|
||||
let mut subject = None;
|
||||
let mut from = None;
|
||||
let replacement;
|
||||
let mut mime = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::Subject) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
subject = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::From) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
from = self.parse_string()?.into();
|
||||
}
|
||||
_ => {
|
||||
replacement = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::Replace(Replace {
|
||||
subject,
|
||||
from,
|
||||
replacement,
|
||||
mime,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_enclose(&mut self) -> Result<(), CompileError> {
|
||||
let mut subject = None;
|
||||
let mut headers = Vec::new();
|
||||
let value;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Subject) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
subject = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Headers) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
headers = self.parse_strings(false)?;
|
||||
}
|
||||
_ => {
|
||||
value = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::Enclose(Enclose {
|
||||
subject,
|
||||
headers,
|
||||
value,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_extracttext(&mut self) -> Result<(), CompileError> {
|
||||
let mut modifiers = Vec::new();
|
||||
let mut first = None;
|
||||
let name;
|
||||
let mut is_local = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::First) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
first = self.tokens.expect_number(usize::MAX)?.into();
|
||||
}
|
||||
Token::Tag(
|
||||
word @ (Word::Lower
|
||||
| Word::Upper
|
||||
| Word::LowerFirst
|
||||
| Word::UpperFirst
|
||||
| Word::QuoteWildcard
|
||||
| Word::QuoteRegex
|
||||
| Word::Length),
|
||||
) => {
|
||||
let modifier = word.into();
|
||||
if !modifiers.contains(&modifier) {
|
||||
modifiers.push(modifier);
|
||||
}
|
||||
}
|
||||
Token::Tag(Word::Replace) => {
|
||||
let find = self.tokens.unwrap_next()?;
|
||||
let replace = self.tokens.unwrap_next()?;
|
||||
modifiers.push(Modifier::Replace {
|
||||
find: self.parse_string_token(find)?,
|
||||
replace: self.parse_string_token(replace)?,
|
||||
});
|
||||
}
|
||||
Token::Tag(Word::Local) => {
|
||||
is_local = true;
|
||||
}
|
||||
_ => {
|
||||
name = self.parse_variable_name(token_info, is_local)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order()));
|
||||
|
||||
self.instructions
|
||||
.push(Instruction::ExtractText(ExtractText {
|
||||
modifiers,
|
||||
first,
|
||||
name,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_mimeopts(&mut self, opts: Word) -> Result<MimeOpts<Value>, CompileError> {
|
||||
Ok(match opts {
|
||||
Word::Type => MimeOpts::Type,
|
||||
Word::Subtype => MimeOpts::Subtype,
|
||||
Word::ContentType => MimeOpts::ContentType,
|
||||
Word::Param => MimeOpts::Param(self.parse_strings(false)?),
|
||||
_ => MimeOpts::None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
FileCarbonCopy,
|
||||
compiler::{
|
||||
CompileError, ErrorType, Value,
|
||||
grammar::{
|
||||
Capability,
|
||||
instruction::{CompilerState, Instruction, MapLocalVars},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
},
|
||||
runtime::actions::action_notify::{validate_from, validate_uri},
|
||||
};
|
||||
|
||||
/*
|
||||
notify [":from" string]
|
||||
[":importance" <"1" / "2" / "3">]
|
||||
[":options" string-list]
|
||||
[":message" string]
|
||||
<method: string>
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Notify {
|
||||
pub from: Option<Value>,
|
||||
pub importance: Option<Value>,
|
||||
pub options: Vec<Value>,
|
||||
pub message: Option<Value>,
|
||||
pub fcc: Option<FileCarbonCopy<Value>>,
|
||||
pub method: Value,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_notify(&mut self) -> Result<(), CompileError> {
|
||||
let method;
|
||||
let mut from = None;
|
||||
let mut importance = None;
|
||||
let mut message = None;
|
||||
let mut options = Vec::new();
|
||||
|
||||
let mut fcc = None;
|
||||
let mut create = false;
|
||||
let mut flags = Vec::new();
|
||||
let mut special_use = None;
|
||||
let mut mailbox_id = None;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::From) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
let address = self.parse_string()?;
|
||||
if let Value::Text(address) = &address
|
||||
&& (address.is_empty() || !validate_from(address))
|
||||
{
|
||||
return Err(token_info.custom(ErrorType::InvalidAddress));
|
||||
}
|
||||
from = address.into();
|
||||
}
|
||||
Token::Tag(Word::Message) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
message = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Importance) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
importance = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Options) => {
|
||||
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
|
||||
options = self.parse_strings(false)?;
|
||||
}
|
||||
Token::Tag(Word::Create) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::Mailbox.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
create = true;
|
||||
}
|
||||
Token::Tag(Word::SpecialUse) => {
|
||||
self.validate_argument(
|
||||
6,
|
||||
Capability::SpecialUse.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
special_use = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::MailboxId) => {
|
||||
self.validate_argument(
|
||||
7,
|
||||
Capability::MailboxId.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mailbox_id = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Fcc) => {
|
||||
self.validate_argument(
|
||||
8,
|
||||
Capability::Fcc.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
fcc = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Flags) => {
|
||||
self.validate_argument(
|
||||
9,
|
||||
Capability::Imap4Flags.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
flags = self.parse_strings(false)?;
|
||||
}
|
||||
_ => {
|
||||
if let Token::StringConstant(uri) = &token_info.token
|
||||
&& validate_uri(uri.to_string().as_ref()).is_none()
|
||||
{
|
||||
return Err(token_info.custom(ErrorType::InvalidURI));
|
||||
}
|
||||
|
||||
method = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fcc.is_none()
|
||||
&& (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some())
|
||||
{
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":fcc"));
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::Notify(Notify {
|
||||
method,
|
||||
from,
|
||||
importance,
|
||||
options,
|
||||
message,
|
||||
fcc: if let Some(fcc) = fcc {
|
||||
FileCarbonCopy {
|
||||
mailbox: fcc,
|
||||
create,
|
||||
flags,
|
||||
special_use,
|
||||
mailbox_id,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapLocalVars for FileCarbonCopy<Value> {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
self.mailbox.map_local_vars(last_id);
|
||||
self.mailbox_id.map_local_vars(last_id);
|
||||
self.flags.map_local_vars(last_id);
|
||||
self.special_use.map_local_vars(last_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{
|
||||
Capability,
|
||||
instruction::{CompilerState, Instruction, MapLocalVars},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Redirect {
|
||||
pub copy: bool,
|
||||
pub address: Value,
|
||||
pub notify: Notify,
|
||||
pub return_of_content: Ret,
|
||||
pub by_time: ByTime<Value>,
|
||||
pub list: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum NotifyItem {
|
||||
Success,
|
||||
Failure,
|
||||
Delay,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum Notify {
|
||||
Never,
|
||||
Items(Vec<NotifyItem>),
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum Ret {
|
||||
Full,
|
||||
Hdrs,
|
||||
Default,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Usage: redirect [:bytimerelative <rlimit: number> /
|
||||
:bytimeabsolute <alimit:string>
|
||||
[:bymode "notify"|"return"] [:bytrace]]
|
||||
<address: string>
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum ByTime<T> {
|
||||
Relative {
|
||||
rlimit: u64,
|
||||
mode: ByMode,
|
||||
trace: bool,
|
||||
},
|
||||
Absolute {
|
||||
alimit: T,
|
||||
mode: ByMode,
|
||||
trace: bool,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum ByMode {
|
||||
Notify,
|
||||
Return,
|
||||
Default,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_redirect(&mut self) -> Result<(), CompileError> {
|
||||
let address;
|
||||
let mut copy = false;
|
||||
let mut ret = Ret::Default;
|
||||
let mut notify = Notify::Default;
|
||||
let mut list = false;
|
||||
let mut by_mode = ByMode::Default;
|
||||
let mut by_trace = false;
|
||||
let mut by_rlimit = None;
|
||||
let mut by_alimit = None;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Copy) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
Capability::Copy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
copy = true;
|
||||
}
|
||||
Token::Tag(Word::List) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
Capability::ExtLists.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
list = true;
|
||||
}
|
||||
Token::Tag(Word::ByTrace) => {
|
||||
self.validate_argument(
|
||||
3,
|
||||
Capability::RedirectDeliverBy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
by_trace = true;
|
||||
}
|
||||
Token::Tag(Word::ByMode) => {
|
||||
self.validate_argument(
|
||||
4,
|
||||
Capability::RedirectDeliverBy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
let by_mode_ = self.tokens.expect_static_string()?;
|
||||
if by_mode_.eq_ignore_ascii_case("notify") {
|
||||
by_mode = ByMode::Notify;
|
||||
} else if by_mode_.eq_ignore_ascii_case("return") {
|
||||
by_mode = ByMode::Return;
|
||||
} else {
|
||||
return Err(token_info.expected("\"notify\" or \"return\""));
|
||||
}
|
||||
}
|
||||
Token::Tag(Word::ByTimeRelative) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::RedirectDeliverBy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
by_rlimit = (self.tokens.expect_number(u64::MAX as usize)? as u64).into();
|
||||
}
|
||||
Token::Tag(Word::ByTimeAbsolute) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::RedirectDeliverBy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
by_alimit = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Ret) => {
|
||||
self.validate_argument(
|
||||
6,
|
||||
Capability::RedirectDsn.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
let ret_ = self.tokens.expect_static_string()?;
|
||||
if ret_.eq_ignore_ascii_case("full") {
|
||||
ret = Ret::Full;
|
||||
} else if ret_.eq_ignore_ascii_case("hdrs") {
|
||||
ret = Ret::Hdrs;
|
||||
} else {
|
||||
return Err(token_info.expected("\"FULL\" or \"HDRS\""));
|
||||
}
|
||||
}
|
||||
Token::Tag(Word::Notify) => {
|
||||
self.validate_argument(
|
||||
7,
|
||||
Capability::RedirectDsn.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
let notify_ = self.tokens.expect_static_string()?;
|
||||
if notify_.eq_ignore_ascii_case("never") {
|
||||
notify = Notify::Never;
|
||||
} else {
|
||||
let mut items = Vec::new();
|
||||
for item in notify_.split(',') {
|
||||
let item = item.trim();
|
||||
if item.eq_ignore_ascii_case("success") {
|
||||
items.push(NotifyItem::Success);
|
||||
} else if item.eq_ignore_ascii_case("failure") {
|
||||
items.push(NotifyItem::Failure);
|
||||
} else if item.eq_ignore_ascii_case("delay") {
|
||||
items.push(NotifyItem::Delay);
|
||||
}
|
||||
}
|
||||
if !items.is_empty() {
|
||||
notify = Notify::Items(items);
|
||||
} else {
|
||||
return Err(
|
||||
token_info.expected("\"NEVER\" or \"SUCCESS, FAILURE, DELAY, ..\"")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
address = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::Redirect(Redirect {
|
||||
address,
|
||||
copy,
|
||||
notify,
|
||||
return_of_content: ret,
|
||||
by_time: if let Some(alimit) = by_alimit {
|
||||
ByTime::Absolute {
|
||||
alimit,
|
||||
mode: by_mode,
|
||||
trace: by_trace,
|
||||
}
|
||||
} else if let Some(rlimit) = by_rlimit {
|
||||
ByTime::Relative {
|
||||
rlimit,
|
||||
mode: by_mode,
|
||||
trace: by_trace,
|
||||
}
|
||||
} else {
|
||||
ByTime::None
|
||||
},
|
||||
list,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapLocalVars for ByTime<Value> {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
if let ByTime::Absolute { alimit, .. } = self {
|
||||
alimit.map_local_vars(last_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::instruction::{CompilerState, Instruction},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Reject {
|
||||
pub ereject: bool,
|
||||
pub reason: Value,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_reject(&mut self, ereject: bool) -> Result<(), CompileError> {
|
||||
let cmd = Instruction::Reject(Reject {
|
||||
ereject,
|
||||
reason: self.parse_string()?,
|
||||
});
|
||||
self.instructions.push(cmd);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError,
|
||||
grammar::{
|
||||
Capability,
|
||||
instruction::{CompilerState, Instruction},
|
||||
},
|
||||
lexer::Token,
|
||||
};
|
||||
|
||||
impl CompilerState<'_> {
|
||||
fn add_capability(&mut self, capabilities: &mut Vec<Capability>, capability: Capability) {
|
||||
if !self.has_capability(&capability) {
|
||||
let parent_capability = if matches!(&capability, Capability::SpamTestPlus) {
|
||||
Some(Capability::SpamTest)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
capabilities.push(capability.clone());
|
||||
self.block.capabilities.insert(capability);
|
||||
|
||||
if let Some(capability) = parent_capability
|
||||
&& !self.has_capability(&capability)
|
||||
{
|
||||
capabilities.push(capability.clone());
|
||||
self.block.capabilities.insert(capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_require(&mut self) -> Result<(), CompileError> {
|
||||
let mut capabilities = Vec::new();
|
||||
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::BracketOpen => loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::StringConstant(value) => {
|
||||
self.add_capability(
|
||||
&mut capabilities,
|
||||
Capability::parse(value.to_string().as_ref()),
|
||||
);
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Comma => (),
|
||||
Token::BracketClose => break,
|
||||
_ => {
|
||||
return Err(token_info.expected("']' or ','"));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(token_info.expected("string"));
|
||||
}
|
||||
}
|
||||
},
|
||||
Token::StringConstant(value) => {
|
||||
self.add_capability(
|
||||
&mut capabilities,
|
||||
Capability::parse(value.to_string().as_ref()),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(token_info.expected("'[' or string"));
|
||||
}
|
||||
}
|
||||
|
||||
if !capabilities.is_empty() {
|
||||
if self.block.require_pos == usize::MAX {
|
||||
self.block.require_pos = self.instructions.len();
|
||||
self.instructions.push(Instruction::Require(capabilities));
|
||||
} else if let Some(Instruction::Require(capabilties)) =
|
||||
self.instructions.get_mut(self.block.require_pos)
|
||||
{
|
||||
capabilties.extend(capabilities)
|
||||
} else {
|
||||
#[cfg(test)]
|
||||
panic!(
|
||||
"Invalid require instruction position {}.",
|
||||
self.block.require_pos
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Envelope,
|
||||
compiler::{
|
||||
CompileError, ErrorType, Value, VariableType,
|
||||
grammar::{
|
||||
expr::Expression,
|
||||
instruction::{CompilerState, Instruction},
|
||||
},
|
||||
lexer::{Token, tokenizer::TokenInfo, word::Word},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Modifier {
|
||||
Lower,
|
||||
Upper,
|
||||
LowerFirst,
|
||||
UpperFirst,
|
||||
QuoteWildcard,
|
||||
QuoteRegex,
|
||||
EncodeUrl,
|
||||
Length,
|
||||
Replace { find: Value, replace: Value },
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
pub fn order(&self) -> usize {
|
||||
match self {
|
||||
Modifier::Lower => 41,
|
||||
Modifier::Upper => 40,
|
||||
Modifier::LowerFirst => 31,
|
||||
Modifier::UpperFirst => 30,
|
||||
Modifier::QuoteWildcard => 20,
|
||||
Modifier::QuoteRegex => 21,
|
||||
Modifier::EncodeUrl => 15,
|
||||
Modifier::Length => 10,
|
||||
Modifier::Replace { .. } => 40,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Set {
|
||||
pub modifiers: Vec<Modifier>,
|
||||
pub name: VariableType,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Let {
|
||||
pub name: VariableType,
|
||||
pub expr: Vec<Expression>,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_set(&mut self) -> Result<(), CompileError> {
|
||||
let mut modifiers = Vec::new();
|
||||
let mut name = None;
|
||||
let mut is_local = false;
|
||||
let value;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Lower
|
||||
| Word::Upper
|
||||
| Word::LowerFirst
|
||||
| Word::UpperFirst
|
||||
| Word::QuoteWildcard
|
||||
| Word::QuoteRegex
|
||||
| Word::Length
|
||||
| Word::EncodeUrl),
|
||||
) => {
|
||||
let modifier = word.into();
|
||||
if !modifiers.contains(&modifier) {
|
||||
modifiers.push(modifier);
|
||||
}
|
||||
}
|
||||
Token::Tag(Word::Replace) => {
|
||||
let find = self.tokens.unwrap_next()?;
|
||||
let replace = self.tokens.unwrap_next()?;
|
||||
modifiers.push(Modifier::Replace {
|
||||
find: self.parse_string_token(find)?,
|
||||
replace: self.parse_string_token(replace)?,
|
||||
});
|
||||
}
|
||||
Token::Tag(Word::Local) => {
|
||||
is_local = true;
|
||||
}
|
||||
_ => {
|
||||
if name.is_none() {
|
||||
name = self.parse_variable_name(token_info, is_local)?.into();
|
||||
} else {
|
||||
value = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order()));
|
||||
|
||||
self.instructions.push(Instruction::Set(Set {
|
||||
modifiers,
|
||||
name: name.unwrap(),
|
||||
value,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_let(&mut self) -> Result<(), CompileError> {
|
||||
let name = self.tokens.unwrap_next()?;
|
||||
let name = self.parse_variable_name(name, false)?;
|
||||
let expr = self.parse_expr()?;
|
||||
|
||||
self.instructions.push(Instruction::Let(Let { name, expr }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_variable_name(
|
||||
&mut self,
|
||||
token_info: TokenInfo,
|
||||
register_as_local: bool,
|
||||
) -> Result<VariableType, CompileError> {
|
||||
match token_info.token {
|
||||
Token::StringConstant(value) => self
|
||||
.register_variable(value.into_string(), register_as_local)
|
||||
.map_err(|error_type| CompileError {
|
||||
line_num: token_info.line_num,
|
||||
line_pos: token_info.line_pos,
|
||||
error_type,
|
||||
}),
|
||||
_ => Err(token_info.custom(ErrorType::ExpectedConstantString)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_variable(
|
||||
&mut self,
|
||||
name: String,
|
||||
register_as_local: bool,
|
||||
) -> Result<VariableType, ErrorType> {
|
||||
let name = name.to_lowercase();
|
||||
if let Some((namespace, part)) = name.split_once('.') {
|
||||
match namespace {
|
||||
"global" | "t" => Ok(VariableType::Global(part.to_string())),
|
||||
"envelope" => Envelope::try_from(part)
|
||||
.map(VariableType::Envelope)
|
||||
.map_err(|_| ErrorType::InvalidNamespace(namespace.to_string())),
|
||||
_ => Err(ErrorType::InvalidNamespace(namespace.to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(if !self.is_var_global(&name) {
|
||||
VariableType::Local(self.register_local_var(name, register_as_local))
|
||||
} else {
|
||||
VariableType::Global(name)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Word> for Modifier {
|
||||
fn from(word: Word) -> Self {
|
||||
match word {
|
||||
Word::Lower => Modifier::Lower,
|
||||
Word::Upper => Modifier::Upper,
|
||||
Word::LowerFirst => Modifier::LowerFirst,
|
||||
Word::UpperFirst => Modifier::UpperFirst,
|
||||
Word::QuoteWildcard => Modifier::QuoteWildcard,
|
||||
Word::QuoteRegex => Modifier::QuoteRegex,
|
||||
Word::Length => Modifier::Length,
|
||||
Word::EncodeUrl => Modifier::EncodeUrl,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
FileCarbonCopy,
|
||||
compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{
|
||||
Capability,
|
||||
instruction::{CompilerState, Instruction},
|
||||
test::Test,
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Vacation {
|
||||
pub subject: Option<Value>,
|
||||
pub from: Option<Value>,
|
||||
pub mime: bool,
|
||||
pub fcc: Option<FileCarbonCopy<Value>>,
|
||||
pub reason: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestVacation {
|
||||
pub addresses: Vec<Value>,
|
||||
pub period: Period,
|
||||
pub handle: Option<Value>,
|
||||
pub reason: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Period {
|
||||
Days(u64),
|
||||
Seconds(u64),
|
||||
Default,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
vacation [":days" number] [":subject" string]
|
||||
[":from" string] [":addresses" string-list]
|
||||
[":mime"] [":handle" string] <reason: string>
|
||||
|
||||
vacation [FCC]
|
||||
[":days" number | ":seconds" number]
|
||||
[":subject" string]
|
||||
[":from" string]
|
||||
[":addresses" string-list]
|
||||
[":mime"]
|
||||
[":handle" string]
|
||||
<reason: string>
|
||||
|
||||
":flags" <list-of-flags: string-list>
|
||||
|
||||
|
||||
FCC = ":fcc" string *FCC-OPTS
|
||||
; per Section 2.6.2 of RFC 5228,
|
||||
; the tagged arguments in FCC may appear in any order
|
||||
|
||||
FCC-OPTS = CREATE / IMAP-FLAGS / SPECIAL-USE
|
||||
; each option MUST NOT appear more than once
|
||||
|
||||
CREATE = ":create"
|
||||
IMAP-FLAGS = ":flags" string-list
|
||||
SPECIAL-USE = ":specialuse" string
|
||||
*/
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_vacation(&mut self) -> Result<(), CompileError> {
|
||||
let mut period = Period::Default;
|
||||
let mut subject = None;
|
||||
let mut from = None;
|
||||
let mut handle = None;
|
||||
let mut addresses = Vec::new();
|
||||
let mut mime = false;
|
||||
let reason;
|
||||
|
||||
let mut fcc = None;
|
||||
let mut create = false;
|
||||
let mut flags = Vec::new();
|
||||
let mut special_use = None;
|
||||
let mut mailbox_id = None;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::Create) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
Capability::Mailbox.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
create = true;
|
||||
}
|
||||
Token::Tag(Word::Days) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
period = Period::Days(self.tokens.expect_number(u64::MAX as usize)? as u64);
|
||||
}
|
||||
Token::Tag(Word::Seconds) => {
|
||||
self.validate_argument(
|
||||
3,
|
||||
Capability::VacationSeconds.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
period = Period::Seconds(self.tokens.expect_number(u64::MAX as usize)? as u64);
|
||||
}
|
||||
Token::Tag(Word::Subject) => {
|
||||
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
|
||||
subject = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::From) => {
|
||||
self.validate_argument(5, None, token_info.line_num, token_info.line_pos)?;
|
||||
from = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Handle) => {
|
||||
self.validate_argument(6, None, token_info.line_num, token_info.line_pos)?;
|
||||
handle = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::SpecialUse) => {
|
||||
self.validate_argument(
|
||||
7,
|
||||
Capability::SpecialUse.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
special_use = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::MailboxId) => {
|
||||
self.validate_argument(
|
||||
8,
|
||||
Capability::MailboxId.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mailbox_id = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Fcc) => {
|
||||
self.validate_argument(
|
||||
9,
|
||||
Capability::Fcc.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
fcc = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Flags) => {
|
||||
self.validate_argument(
|
||||
10,
|
||||
Capability::Imap4Flags.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
flags = self.parse_strings(false)?;
|
||||
}
|
||||
Token::Tag(Word::Addresses) => {
|
||||
self.validate_argument(11, None, token_info.line_num, token_info.line_pos)?;
|
||||
addresses = self.parse_strings(false)?;
|
||||
}
|
||||
_ => {
|
||||
reason = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fcc.is_none()
|
||||
&& (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some())
|
||||
{
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":fcc"));
|
||||
}
|
||||
|
||||
self.instructions
|
||||
.push(Instruction::Test(Test::Vacation(TestVacation {
|
||||
period,
|
||||
handle,
|
||||
reason: reason.clone(),
|
||||
addresses,
|
||||
})));
|
||||
|
||||
self.instructions
|
||||
.push(Instruction::Jz(self.instructions.len() + 2));
|
||||
|
||||
self.instructions.push(Instruction::Vacation(Vacation {
|
||||
reason,
|
||||
subject,
|
||||
from,
|
||||
mime,
|
||||
fcc: if let Some(fcc) = fcc {
|
||||
FileCarbonCopy {
|
||||
mailbox: fcc,
|
||||
create,
|
||||
flags,
|
||||
special_use,
|
||||
mailbox_id,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod action_convert;
|
||||
pub mod action_editheader;
|
||||
pub mod action_fileinto;
|
||||
pub mod action_flags;
|
||||
pub mod action_include;
|
||||
pub mod action_keep;
|
||||
pub mod action_mime;
|
||||
pub mod action_notify;
|
||||
pub mod action_redirect;
|
||||
pub mod action_reject;
|
||||
pub mod action_require;
|
||||
pub mod action_set;
|
||||
pub mod action_vacation;
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::compiler::{Number, VariableType};
|
||||
|
||||
pub mod parser;
|
||||
pub mod tokenizer;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Expression {
|
||||
Variable(VariableType),
|
||||
Constant(Constant),
|
||||
BinaryOperator(BinaryOperator),
|
||||
UnaryOperator(UnaryOperator),
|
||||
JmpIf { val: bool, pos: u32 },
|
||||
Function { id: u32, num_args: u32 },
|
||||
ArrayAccess,
|
||||
ArrayBuild(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Constant {
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(Arc<String>),
|
||||
}
|
||||
|
||||
impl Eq for Constant {}
|
||||
|
||||
impl From<Number> for Constant {
|
||||
fn from(value: Number) -> Self {
|
||||
match value {
|
||||
Number::Integer(i) => Constant::Integer(i),
|
||||
Number::Float(f) => Constant::Float(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Constant {
|
||||
fn from(value: String) -> Self {
|
||||
Constant::String(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum BinaryOperator {
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum UnaryOperator {
|
||||
Not,
|
||||
Minus,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub(crate) enum Token {
|
||||
Variable(VariableType),
|
||||
Function {
|
||||
name: String,
|
||||
id: u32,
|
||||
num_args: u32,
|
||||
},
|
||||
Number(Number),
|
||||
String(String),
|
||||
BinaryOperator(BinaryOperator),
|
||||
UnaryOperator(UnaryOperator),
|
||||
OpenParen,
|
||||
CloseParen,
|
||||
OpenBracket,
|
||||
CloseBracket,
|
||||
Comma,
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{BinaryOperator, Expression, Token, tokenizer::Tokenizer};
|
||||
|
||||
pub(crate) struct ExpressionParser<'x, F>
|
||||
where
|
||||
F: Fn(&str, bool) -> Result<Token, String>,
|
||||
{
|
||||
pub(crate) tokenizer: Tokenizer<'x, F>,
|
||||
pub(crate) output: Vec<Expression>,
|
||||
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;
|
||||
pub(crate) const ID_EXTERNAL: u32 = u32::MAX - 2;
|
||||
|
||||
impl<'x, F> ExpressionParser<'x, F>
|
||||
where
|
||||
F: Fn(&str, bool) -> Result<Token, String>,
|
||||
{
|
||||
pub fn from_tokenizer(tokenizer: Tokenizer<'x, F>) -> Self {
|
||||
Self {
|
||||
tokenizer,
|
||||
output: Vec::new(),
|
||||
operator_stack: Vec::new(),
|
||||
arg_count: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Result<Self, 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(Expression::Variable(v))
|
||||
}
|
||||
Token::Number(n) => {
|
||||
self.inc_arg_count();
|
||||
self.output.push(Expression::Constant(n.into()))
|
||||
}
|
||||
Token::String(s) => {
|
||||
self.inc_arg_count();
|
||||
self.output.push(Expression::Constant(s.into()))
|
||||
}
|
||||
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(Expression::BinaryOperator(bop))
|
||||
}
|
||||
Some((Token::UnaryOperator(uop), _)) => {
|
||||
self.output.push(Expression::UnaryOperator(uop))
|
||||
}
|
||||
_ => return Err("Mismatched parentheses".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((Token::Function { id, num_args, name }, _)) =
|
||||
self.operator_stack.last()
|
||||
{
|
||||
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 => Expression::ArrayAccess,
|
||||
ID_ARRAY_BUILD => Expression::ArrayBuild(*num_args),
|
||||
id => Expression::Function {
|
||||
id,
|
||||
num_args: *num_args,
|
||||
},
|
||||
};
|
||||
|
||||
self.operator_stack.pop();
|
||||
self.output.push(expr);
|
||||
}
|
||||
|
||||
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(Expression::BinaryOperator(top_bop));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Token::UnaryOperator(top_uop) => {
|
||||
let top_uop = *top_uop;
|
||||
self.operator_stack.pop();
|
||||
self.output.push(Expression::UnaryOperator(top_uop));
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Add jump instruction for short-circuiting
|
||||
let jmp_pos = match bop {
|
||||
BinaryOperator::And => {
|
||||
self.output.push(Expression::JmpIf { val: false, pos: 0 });
|
||||
Some(self.output.len() - 1)
|
||||
}
|
||||
BinaryOperator::Or => {
|
||||
self.output.push(Expression::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::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: String::from("array"),
|
||||
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(Expression::BinaryOperator(bop));
|
||||
self.operator_stack.pop();
|
||||
}
|
||||
Token::UnaryOperator(uop) => {
|
||||
self.output.push(Expression::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(Expression::BinaryOperator(bop))
|
||||
}
|
||||
Token::UnaryOperator(uop) => self.output.push(Expression::UnaryOperator(uop)),
|
||||
_ => return Err("Invalid token on the operator stack".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
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 Expression::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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
iter::{Enumerate, Peekable},
|
||||
slice::Iter,
|
||||
};
|
||||
|
||||
use crate::{compiler::Number, runtime::eval::IntoString};
|
||||
|
||||
use super::{BinaryOperator, Token, UnaryOperator};
|
||||
|
||||
pub(crate) struct Tokenizer<'x, F>
|
||||
where
|
||||
F: Fn(&str, bool) -> Result<Token, String>,
|
||||
{
|
||||
pub(crate) iter: Peekable<Enumerate<Iter<'x, u8>>>,
|
||||
token_map: F,
|
||||
buf: Vec<u8>,
|
||||
depth: u32,
|
||||
next_token: Vec<Token>,
|
||||
has_number: bool,
|
||||
has_dot: bool,
|
||||
has_alpha: bool,
|
||||
is_start: bool,
|
||||
is_eof: bool,
|
||||
}
|
||||
|
||||
impl<'x, F> Tokenizer<'x, F>
|
||||
where
|
||||
F: Fn(&str, bool) -> Result<Token, String>,
|
||||
{
|
||||
#[cfg(test)]
|
||||
pub fn new(expr: &'x str, token_map: F) -> Self {
|
||||
Self::from_iter(expr.as_bytes().iter().enumerate().peekable(), token_map)
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub(crate) fn from_iter(iter: Peekable<Enumerate<Iter<'x, u8>>>, token_map: F) -> Self {
|
||||
Self {
|
||||
iter,
|
||||
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(crate) 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'_' => {
|
||||
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 matches!(self.buf.get(0..7), Some(b"header.")) => {
|
||||
self.buf.push(ch);
|
||||
}
|
||||
b'-' if self.buf.last().is_some_and(|c| *c == b'[')
|
||||
|| matches!(self.buf.get(0..7), Some(b"header.")) =>
|
||||
{
|
||||
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().is_some_and(|&c| c == b'[' || c == b'.') => {
|
||||
self.buf.push(ch);
|
||||
}
|
||||
_ => {
|
||||
let prev_token = if !self.buf.is_empty() {
|
||||
self.is_start = false;
|
||||
self.parse_buf()?.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
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'\'' => {
|
||||
let mut buf = Vec::with_capacity(16);
|
||||
let stop_ch = ch;
|
||||
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 {
|
||||
Token::String(
|
||||
String::from_utf8(buf)
|
||||
.map_err(|_| "Invalid UTF-8".to_string())?,
|
||||
)
|
||||
} else {
|
||||
return Err("Unterminated string".to_string());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
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 parse_buf(&mut self) -> Result<Token, String> {
|
||||
let buf = std::mem::take(&mut self.buf).into_string();
|
||||
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::Number(Number::Float(f)))
|
||||
.map_err(|_| format!("Invalid float value {}", buf,))
|
||||
} else {
|
||||
buf.parse::<i64>()
|
||||
.map(|i| Token::Number(Number::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::Number(Number::Integer(1)));
|
||||
} else if buf == "false" {
|
||||
return Ok(Token::Number(Number::Integer(0)));
|
||||
}
|
||||
}
|
||||
|
||||
(self.token_map)(&buf, has_dot)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1221
File diff suppressed because it is too large
Load Diff
+736
@@ -0,0 +1,736 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use self::{expr::Expression, instruction::CompilerState};
|
||||
|
||||
use super::{
|
||||
CompileError, ErrorType, Regex, Value,
|
||||
lexer::{Token, tokenizer::TokenInfo, word::Word},
|
||||
};
|
||||
|
||||
pub mod actions;
|
||||
pub mod expr;
|
||||
pub mod instruction;
|
||||
pub mod test;
|
||||
pub mod tests;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
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
|
||||
Expressions,
|
||||
While,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum AddressPart {
|
||||
LocalPart,
|
||||
Domain,
|
||||
All,
|
||||
User,
|
||||
Detail,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum MatchType {
|
||||
Is,
|
||||
Contains,
|
||||
Matches(u64),
|
||||
Regex(u64),
|
||||
Value(RelationalMatch),
|
||||
Count(RelationalMatch),
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum RelationalMatch {
|
||||
Gt,
|
||||
Ge,
|
||||
Lt,
|
||||
Le,
|
||||
Eq,
|
||||
Ne,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum Comparator {
|
||||
Elbonia,
|
||||
Octet,
|
||||
AsciiCaseMap,
|
||||
AsciiNumeric,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
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)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub struct Invalid {
|
||||
pub(crate) name: String,
|
||||
pub(crate) line_num: usize,
|
||||
pub(crate) line_pos: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct While {
|
||||
pub expr: Vec<Expression>,
|
||||
pub jz_pos: usize,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
#[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
|
||||
&& let Some(relational) = lookup_relational(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) = lookup_comparator(&comparator) {
|
||||
comparator
|
||||
} 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))
|
||||
|| (capability != &Capability::Ihave && self.compiler.no_capability_check)
|
||||
}
|
||||
|
||||
#[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
|
||||
&& !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::new(expr.to_string(), regex));
|
||||
}
|
||||
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) = lookup_capabilities(capability) {
|
||||
capability
|
||||
} 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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_relational(input: &str) -> Option<RelationalMatch> {
|
||||
hashify::tiny_map!(
|
||||
input.as_bytes(),
|
||||
"gt" => RelationalMatch::Gt,
|
||||
"ge" => RelationalMatch::Ge,
|
||||
"lt" => RelationalMatch::Lt,
|
||||
"le" => RelationalMatch::Le,
|
||||
"eq" => RelationalMatch::Eq,
|
||||
"ne" => RelationalMatch::Ne,
|
||||
)
|
||||
}
|
||||
|
||||
fn lookup_comparator(input: &str) -> Option<Comparator> {
|
||||
hashify::tiny_map!(
|
||||
input.as_bytes(),
|
||||
"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::While => f.write_str("vnd.inbuxa.while"),
|
||||
Capability::Expressions => f.write_str("vnd.inbuxa.expressions"),
|
||||
Capability::Other(capability) => f.write_str(capability),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_capabilities(input: &str) -> Option<Capability> {
|
||||
hashify::tiny_map!(
|
||||
input.as_bytes(),
|
||||
"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.inbuxa.while" => Capability::While,
|
||||
"vnd.inbuxa.expressions" => Capability::Expressions,
|
||||
)
|
||||
}
|
||||
+686
@@ -0,0 +1,686 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType,
|
||||
lexer::{Token, tokenizer::TokenInfo, word::Word},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Capability, Invalid,
|
||||
actions::{action_convert::Convert, action_vacation::TestVacation},
|
||||
expr::{Expression, UnaryOperator, parser::ExpressionParser, tokenizer::Tokenizer},
|
||||
instruction::{CompilerState, Instruction},
|
||||
tests::{
|
||||
test_address::TestAddress,
|
||||
test_body::TestBody,
|
||||
test_date::{TestCurrentDate, TestDate},
|
||||
test_duplicate::TestDuplicate,
|
||||
test_envelope::TestEnvelope,
|
||||
test_exists::TestExists,
|
||||
test_extlists::TestValidExtList,
|
||||
test_hasflag::TestHasFlag,
|
||||
test_header::TestHeader,
|
||||
test_ihave::TestIhave,
|
||||
test_mailbox::{TestMailboxExists, TestMetadata, TestMetadataExists},
|
||||
test_mailboxid::TestMailboxIdExists,
|
||||
test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod},
|
||||
test_size::TestSize,
|
||||
test_spamtest::{TestSpamTest, TestVirusTest},
|
||||
test_specialuse::TestSpecialUseExists,
|
||||
test_string::TestString,
|
||||
},
|
||||
};
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Test {
|
||||
True,
|
||||
False,
|
||||
Address(TestAddress),
|
||||
Envelope(TestEnvelope),
|
||||
Exists(TestExists),
|
||||
Header(TestHeader),
|
||||
Size(TestSize),
|
||||
Invalid(Invalid),
|
||||
|
||||
// RFC 5173
|
||||
Body(TestBody),
|
||||
|
||||
// RFC 6558
|
||||
Convert(Convert),
|
||||
|
||||
// RFC 5260
|
||||
Date(TestDate),
|
||||
CurrentDate(TestCurrentDate),
|
||||
|
||||
// RFC 7352
|
||||
Duplicate(TestDuplicate),
|
||||
|
||||
// RFC 5229 & RFC 5183
|
||||
String(TestString),
|
||||
Environment(TestString),
|
||||
|
||||
// RFC 5435
|
||||
NotifyMethodCapability(TestNotifyMethodCapability),
|
||||
ValidNotifyMethod(TestValidNotifyMethod),
|
||||
|
||||
// RFC 6134
|
||||
ValidExtList(TestValidExtList),
|
||||
|
||||
// RFC 5463
|
||||
Ihave(TestIhave),
|
||||
|
||||
// RFC 5232
|
||||
HasFlag(TestHasFlag),
|
||||
|
||||
// RFC 5490
|
||||
MailboxExists(TestMailboxExists),
|
||||
Metadata(TestMetadata),
|
||||
MetadataExists(TestMetadataExists),
|
||||
|
||||
// RFC 9042
|
||||
MailboxIdExists(TestMailboxIdExists),
|
||||
|
||||
// RFC 5235
|
||||
SpamTest(TestSpamTest),
|
||||
VirusTest(TestVirusTest),
|
||||
|
||||
// RFC 8579
|
||||
SpecialUseExists(TestSpecialUseExists),
|
||||
|
||||
// RFC 5230
|
||||
Vacation(TestVacation),
|
||||
|
||||
// Only test
|
||||
#[cfg(test)]
|
||||
TestCmd {
|
||||
arguments: Vec<crate::compiler::Value>,
|
||||
is_not: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Block {
|
||||
is_all: bool,
|
||||
is_not: bool,
|
||||
p_count: u32,
|
||||
jmps: Vec<usize>,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test(&mut self) -> Result<(), CompileError> {
|
||||
let mut block_stack: Vec<Block> = Vec::new();
|
||||
let mut block = Block {
|
||||
is_all: false,
|
||||
is_not: false,
|
||||
p_count: 0,
|
||||
jmps: Vec::new(),
|
||||
};
|
||||
let mut is_not = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
self.reset_param_check();
|
||||
let test: Instruction =
|
||||
match token_info.token {
|
||||
Token::Comma
|
||||
if !block_stack.is_empty()
|
||||
&& matches!(
|
||||
self.instructions.last(),
|
||||
Some(Instruction::Test(_) | Instruction::Eval(_))
|
||||
)
|
||||
&& matches!(
|
||||
self.tokens.peek(),
|
||||
Some(Ok(TokenInfo {
|
||||
token: Token::Identifier(_) | Token::Unknown(_),
|
||||
..
|
||||
}))
|
||||
) =>
|
||||
{
|
||||
is_not = block.is_not;
|
||||
block.jmps.push(self.instructions.len());
|
||||
self.instructions.push(if block.is_all {
|
||||
Instruction::Jz(usize::MAX)
|
||||
} else {
|
||||
Instruction::Jnz(usize::MAX)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
Token::ParenthesisOpen => {
|
||||
block.p_count += 1;
|
||||
continue;
|
||||
}
|
||||
Token::ParenthesisClose => {
|
||||
if block.p_count > 0 {
|
||||
block.p_count -= 1;
|
||||
continue;
|
||||
} else if let Some(prev_block) = block_stack.pop() {
|
||||
let cur_pos = self.instructions.len();
|
||||
for jmp_pos in block.jmps {
|
||||
if let Instruction::Jnz(jmp_pos) | Instruction::Jz(jmp_pos) =
|
||||
&mut self.instructions[jmp_pos]
|
||||
{
|
||||
*jmp_pos = cur_pos;
|
||||
} else {
|
||||
debug_assert!(false, "This should not have happened")
|
||||
}
|
||||
}
|
||||
|
||||
block = prev_block;
|
||||
is_not = block.is_not;
|
||||
if block_stack.is_empty() {
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
return Err(token_info.expected("test name"));
|
||||
}
|
||||
}
|
||||
Token::Identifier(Word::Not) => {
|
||||
if !matches!(
|
||||
self.tokens.peek(),
|
||||
Some(Ok(TokenInfo {
|
||||
token: Token::Identifier(_) | Token::Unknown(_),
|
||||
..
|
||||
}))
|
||||
) {
|
||||
return Err(token_info.expected("test name"));
|
||||
}
|
||||
is_not = !is_not;
|
||||
continue;
|
||||
}
|
||||
Token::Identifier(word @ (Word::AnyOf | Word::AllOf)) => {
|
||||
if block_stack.len() < self.tokens.compiler.max_nested_tests {
|
||||
self.tokens.expect_token(Token::ParenthesisOpen)?;
|
||||
block_stack.push(block);
|
||||
let (is_all, block_is_not) = if word == Word::AllOf {
|
||||
if !is_not {
|
||||
(true, false)
|
||||
} else {
|
||||
(false, true)
|
||||
}
|
||||
} else if !is_not {
|
||||
(false, false)
|
||||
} else {
|
||||
(true, true)
|
||||
};
|
||||
block = Block {
|
||||
is_all,
|
||||
is_not: block_is_not,
|
||||
p_count: 0,
|
||||
jmps: Vec::new(),
|
||||
};
|
||||
is_not = block_is_not;
|
||||
continue;
|
||||
} else {
|
||||
return Err(CompileError {
|
||||
line_num: token_info.line_num,
|
||||
line_pos: token_info.line_pos,
|
||||
error_type: ErrorType::TooManyNestedTests,
|
||||
});
|
||||
}
|
||||
}
|
||||
Token::Identifier(Word::True) => if !is_not {
|
||||
Test::True
|
||||
} else {
|
||||
is_not = false;
|
||||
Test::False
|
||||
}
|
||||
.into(),
|
||||
Token::Identifier(Word::False) => if !is_not {
|
||||
Test::False
|
||||
} else {
|
||||
is_not = false;
|
||||
Test::True
|
||||
}
|
||||
.into(),
|
||||
Token::Identifier(Word::Address) => self.parse_test_address()?.into(),
|
||||
Token::Identifier(Word::Envelope) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Envelope.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_envelope()?.into()
|
||||
}
|
||||
Token::Identifier(Word::Header) => self.parse_test_header()?.into(),
|
||||
Token::Identifier(Word::Size) => self.parse_test_size()?.into(),
|
||||
Token::Identifier(Word::Exists) => self.parse_test_exists()?.into(),
|
||||
|
||||
// RFC 5173
|
||||
Token::Identifier(Word::Body) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Body.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_body()?.into()
|
||||
}
|
||||
|
||||
// RFC 6558
|
||||
Token::Identifier(Word::Convert) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Convert.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_convert()?.into()
|
||||
}
|
||||
|
||||
// RFC 5260
|
||||
Token::Identifier(Word::Date) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Date.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_date()?.into()
|
||||
}
|
||||
Token::Identifier(Word::CurrentDate) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Date.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_currentdate()?.into()
|
||||
}
|
||||
|
||||
// RFC 7352
|
||||
Token::Identifier(Word::Duplicate) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Duplicate.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_duplicate()?.into()
|
||||
}
|
||||
|
||||
// RFC 5229
|
||||
Token::Identifier(Word::String) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Variables.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_string()?.into()
|
||||
}
|
||||
|
||||
// RFC 5435
|
||||
Token::Identifier(Word::NotifyMethodCapability) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Enotify.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_notify_method_capability()?.into()
|
||||
}
|
||||
Token::Identifier(Word::ValidNotifyMethod) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Enotify.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_valid_notify_method()?.into()
|
||||
}
|
||||
|
||||
// RFC 5183
|
||||
Token::Identifier(Word::Environment) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Environment.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_environment()?.into()
|
||||
}
|
||||
|
||||
// RFC 6134
|
||||
Token::Identifier(Word::ValidExtList) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::ExtLists.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_valid_ext_list()?.into()
|
||||
}
|
||||
|
||||
// RFC 5463
|
||||
Token::Identifier(Word::Ihave) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Ihave.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_ihave()?.into()
|
||||
}
|
||||
|
||||
// RFC 5232
|
||||
Token::Identifier(Word::HasFlag) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Imap4Flags.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_hasflag()?.into()
|
||||
}
|
||||
|
||||
// RFC 5490
|
||||
Token::Identifier(Word::MailboxExists) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Mailbox.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_mailboxexists()?.into()
|
||||
}
|
||||
Token::Identifier(Word::Metadata) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::MboxMetadata.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_metadata()?.into()
|
||||
}
|
||||
Token::Identifier(Word::MetadataExists) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::MboxMetadata.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_metadataexists()?.into()
|
||||
}
|
||||
Token::Identifier(Word::ServerMetadata) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::ServerMetadata.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_servermetadata()?.into()
|
||||
}
|
||||
Token::Identifier(Word::ServerMetadataExists) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::ServerMetadata.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_servermetadataexists()?.into()
|
||||
}
|
||||
|
||||
// RFC 9042
|
||||
Token::Identifier(Word::MailboxIdExists) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::MailboxId.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_mailboxidexists()?.into()
|
||||
}
|
||||
|
||||
// RFC 5235
|
||||
Token::Identifier(Word::SpamTest) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::SpamTest.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_spamtest()?.into()
|
||||
}
|
||||
Token::Identifier(Word::VirusTest) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::VirusTest.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_virustest()?.into()
|
||||
}
|
||||
|
||||
// RFC 8579
|
||||
Token::Identifier(Word::SpecialUseExists) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::SpecialUse.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
self.parse_test_specialuseexists()?.into()
|
||||
}
|
||||
|
||||
// Expressions extension
|
||||
Token::Identifier(Word::Eval) => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::Expressions.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
Instruction::Eval(self.parse_expr()?)
|
||||
}
|
||||
Token::Identifier(word) => {
|
||||
self.ignore_test()?;
|
||||
Test::Invalid(Invalid {
|
||||
name: word.to_string(),
|
||||
line_num: token_info.line_num,
|
||||
line_pos: token_info.line_pos,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
#[cfg(test)]
|
||||
Token::Unknown(name) if name.contains("test") => {
|
||||
use crate::compiler::Value;
|
||||
|
||||
let mut arguments = Vec::new();
|
||||
arguments.push(Value::Text(name.into()));
|
||||
while !matches!(
|
||||
self.tokens.peek().map(|r| r.map(|t| &t.token)),
|
||||
Some(Ok(Token::Comma
|
||||
| Token::ParenthesisClose
|
||||
| Token::CurlyOpen))
|
||||
) {
|
||||
arguments.push(match self.tokens.unwrap_next()?.token {
|
||||
Token::StringConstant(s) => Value::from(s),
|
||||
Token::StringVariable(s) => self
|
||||
.tokenize_string(&s, true)
|
||||
.map_err(|error_type| CompileError {
|
||||
line_num: 0,
|
||||
line_pos: 0,
|
||||
error_type,
|
||||
})?,
|
||||
Token::Number(n) => {
|
||||
Value::Number(crate::compiler::Number::Integer(n as i64))
|
||||
}
|
||||
Token::Identifier(s) => Value::Text(s.to_string().into()),
|
||||
Token::Tag(s) => Value::Text(format!(":{s}").into()),
|
||||
Token::Unknown(s) => Value::Text(s.into()),
|
||||
other => panic!("Invalid test param {other:?}"),
|
||||
});
|
||||
}
|
||||
Test::TestCmd {
|
||||
arguments,
|
||||
is_not: false,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
Token::Unknown(name) => {
|
||||
self.ignore_test()?;
|
||||
Test::Invalid(Invalid {
|
||||
name,
|
||||
line_num: token_info.line_num,
|
||||
line_pos: token_info.line_pos,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
_ => return Err(token_info.expected("test name")),
|
||||
};
|
||||
|
||||
while block.p_count > 0 {
|
||||
self.tokens.expect_token(Token::ParenthesisClose)?;
|
||||
block.p_count -= 1;
|
||||
}
|
||||
|
||||
self.instructions
|
||||
.push(if !is_not { test } else { test.set_not() });
|
||||
|
||||
if block_stack.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.instructions.push(Instruction::Jz(usize::MAX));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_expr(&mut self) -> Result<Vec<Expression>, CompileError> {
|
||||
let mut next_token = self.tokens.unwrap_next()?;
|
||||
let expr = match next_token.token {
|
||||
Token::StringConstant(s) => s.into_string().into_bytes(),
|
||||
Token::StringVariable(s) => s,
|
||||
_ => return Err(next_token.expected("string")),
|
||||
};
|
||||
|
||||
match ExpressionParser::from_tokenizer(Tokenizer::from_iter(
|
||||
expr.iter().enumerate().peekable(),
|
||||
|var_name, maybe_namespace| self.parse_expr_fnc_or_var(var_name, maybe_namespace),
|
||||
))
|
||||
.parse()
|
||||
{
|
||||
Ok(parser) => Ok(parser.output),
|
||||
Err(err) => {
|
||||
let err = ErrorType::InvalidExpression(format!(
|
||||
"{}: {}",
|
||||
std::str::from_utf8(&expr).unwrap_or_default(),
|
||||
err
|
||||
));
|
||||
next_token.token = Token::StringVariable(expr);
|
||||
Err(next_token.custom(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Test> for Instruction {
|
||||
fn from(test: Test) -> Self {
|
||||
Instruction::Test(test)
|
||||
}
|
||||
}
|
||||
|
||||
impl Instruction {
|
||||
pub fn set_not(mut self) -> Self {
|
||||
match &mut self {
|
||||
Instruction::Test(test) => match test {
|
||||
Test::True => return Instruction::Test(Test::False),
|
||||
Test::False => return Instruction::Test(Test::True),
|
||||
Test::Address(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Envelope(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Exists(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Header(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Size(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Body(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Convert(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Date(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::CurrentDate(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Duplicate(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::String(op) | Test::Environment(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::NotifyMethodCapability(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::ValidNotifyMethod(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::ValidExtList(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Ihave(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::HasFlag(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::MailboxExists(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::Metadata(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::MetadataExists(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::MailboxIdExists(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::SpamTest(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::VirusTest(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
Test::SpecialUseExists(op) => {
|
||||
op.is_not = true;
|
||||
}
|
||||
#[cfg(test)]
|
||||
Test::TestCmd { is_not, .. } => {
|
||||
*is_not = true;
|
||||
}
|
||||
Test::Vacation(_) | Test::Invalid(_) => {}
|
||||
},
|
||||
Instruction::Eval(expr) => expr.push(Expression::UnaryOperator(UnaryOperator::Not)),
|
||||
_ => (),
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod test_address;
|
||||
pub mod test_body;
|
||||
pub mod test_date;
|
||||
pub mod test_duplicate;
|
||||
pub mod test_envelope;
|
||||
pub mod test_environment;
|
||||
pub mod test_exists;
|
||||
pub mod test_extlists;
|
||||
pub mod test_hasflag;
|
||||
pub mod test_header;
|
||||
pub mod test_ihave;
|
||||
pub mod test_mailbox;
|
||||
pub mod test_mailboxid;
|
||||
pub mod test_notify;
|
||||
pub mod test_size;
|
||||
pub mod test_spamtest;
|
||||
pub mod test_specialuse;
|
||||
pub mod test_string;
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState, test::Test},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{AddressPart, MatchType};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestAddress {
|
||||
pub header_list: Vec<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub address_part: AddressPart,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub index: Option<i32>,
|
||||
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_address(&mut self) -> Result<Test, CompileError> {
|
||||
let mut address_part = AddressPart::All;
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut header_list = None;
|
||||
let mut key_list;
|
||||
let mut index = None;
|
||||
let mut index_last = false;
|
||||
|
||||
let mut mime = false;
|
||||
let mut mime_anychild = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::LocalPart
|
||||
| Word::Domain
|
||||
| Word::All
|
||||
| Word::User
|
||||
| Word::Detail
|
||||
| Word::Name),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
if matches!(word, Word::User | Word::Detail) {
|
||||
Capability::SubAddress.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
address_part = word.into();
|
||||
}
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex
|
||||
| Word::List),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Index) => {
|
||||
self.validate_argument(
|
||||
4,
|
||||
Capability::Index.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
|
||||
}
|
||||
Token::Tag(Word::Last) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::Index.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
index_last = true;
|
||||
}
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(
|
||||
6,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::AnyChild) => {
|
||||
self.validate_argument(
|
||||
7,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime_anychild = true;
|
||||
}
|
||||
_ => {
|
||||
if header_list.is_none() {
|
||||
header_list = self.parse_strings_token(token_info)?.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mime && mime_anychild {
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Address(TestAddress {
|
||||
header_list: header_list.unwrap(),
|
||||
key_list,
|
||||
address_part,
|
||||
match_type,
|
||||
comparator,
|
||||
index: if index_last { index.map(|i| -i) } else { index },
|
||||
mime_anychild,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Word> for AddressPart {
|
||||
fn from(word: Word) -> Self {
|
||||
match word {
|
||||
Word::LocalPart => AddressPart::LocalPart,
|
||||
Word::Domain => AddressPart::Domain,
|
||||
Word::All => AddressPart::All,
|
||||
Word::User => AddressPart::User,
|
||||
Word::Detail => AddressPart::Detail,
|
||||
Word::Name => AddressPart::Name,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestBody {
|
||||
pub key_list: Vec<Value>,
|
||||
pub body_transform: BodyTransform,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub include_subject: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum BodyTransform {
|
||||
Raw,
|
||||
Content(Vec<Value>),
|
||||
Text,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_body(&mut self) -> Result<Test, CompileError> {
|
||||
let mut body_transform = BodyTransform::Text;
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut key_list;
|
||||
let mut include_subject = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Raw) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
body_transform = BodyTransform::Raw;
|
||||
}
|
||||
Token::Tag(Word::Text) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
body_transform = BodyTransform::Text;
|
||||
}
|
||||
Token::Tag(Word::Content) => {
|
||||
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
|
||||
body_transform = BodyTransform::Content(self.parse_strings(false)?);
|
||||
}
|
||||
Token::Tag(Word::Subject) => {
|
||||
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
|
||||
include_subject = true;
|
||||
}
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Body(TestBody {
|
||||
key_list,
|
||||
body_transform,
|
||||
match_type,
|
||||
comparator,
|
||||
include_subject,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Number, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{StringConstant, Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestDate {
|
||||
pub header_name: Value,
|
||||
pub key_list: Vec<Value>,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub index: Option<i32>,
|
||||
pub zone: Zone,
|
||||
pub date_part: DatePart,
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestCurrentDate {
|
||||
pub zone: Option<i64>,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub date_part: DatePart,
|
||||
pub key_list: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum Zone {
|
||||
Time(i64),
|
||||
Original,
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum DatePart {
|
||||
Year,
|
||||
Month,
|
||||
Day,
|
||||
Date,
|
||||
Julian,
|
||||
Hour,
|
||||
Minute,
|
||||
Second,
|
||||
Time,
|
||||
Iso8601,
|
||||
Std11,
|
||||
Zone,
|
||||
Weekday,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_date(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut header_name = None;
|
||||
let mut key_list;
|
||||
let mut index = None;
|
||||
let mut index_last = false;
|
||||
let mut zone = Zone::Local;
|
||||
let mut date_part = None;
|
||||
|
||||
let mut mime = false;
|
||||
let mut mime_anychild = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex
|
||||
| Word::List),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Index) => {
|
||||
self.validate_argument(
|
||||
3,
|
||||
Capability::Index.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
|
||||
}
|
||||
Token::Tag(Word::Last) => {
|
||||
self.validate_argument(
|
||||
4,
|
||||
Capability::Index.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
index_last = true;
|
||||
}
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::AnyChild) => {
|
||||
self.validate_argument(
|
||||
6,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime_anychild = true;
|
||||
}
|
||||
Token::Tag(Word::OriginalZone) => {
|
||||
self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?;
|
||||
zone = Zone::Original;
|
||||
}
|
||||
Token::Tag(Word::Zone) => {
|
||||
self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?;
|
||||
zone = Zone::Time(self.parse_timezone()?);
|
||||
}
|
||||
_ => {
|
||||
if header_name.is_none() {
|
||||
let header = self.parse_string_token(token_info)?;
|
||||
if let Value::Text(header_name) = &header
|
||||
&& HeaderName::parse(header_name.as_ref()).is_none()
|
||||
{
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::InvalidHeaderName));
|
||||
}
|
||||
header_name = header.into();
|
||||
} else if date_part.is_none() {
|
||||
if let Token::StringConstant(string) = &token_info.token
|
||||
&& let Some(date_part_) =
|
||||
lookup_date_part(&string.to_string().to_ascii_lowercase())
|
||||
{
|
||||
date_part = date_part_.into();
|
||||
continue;
|
||||
}
|
||||
return Err(token_info.expected("valid date part"));
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mime && mime_anychild {
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Date(TestDate {
|
||||
header_name: header_name.unwrap(),
|
||||
key_list,
|
||||
date_part: date_part.unwrap(),
|
||||
match_type,
|
||||
comparator,
|
||||
index: if index_last { index.map(|i| -i) } else { index },
|
||||
zone,
|
||||
mime_anychild,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_currentdate(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut key_list;
|
||||
let mut zone = None;
|
||||
let mut date_part = None;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex
|
||||
| Word::List),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Zone) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
zone = self.parse_timezone()?.into();
|
||||
}
|
||||
_ => {
|
||||
if date_part.is_none() {
|
||||
if let Token::StringConstant(string) = &token_info.token
|
||||
&& let Some(date_part_) =
|
||||
lookup_date_part(&string.to_string().to_ascii_lowercase())
|
||||
{
|
||||
date_part = date_part_.into();
|
||||
continue;
|
||||
}
|
||||
return Err(token_info.expected("valid date part"));
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::CurrentDate(TestCurrentDate {
|
||||
key_list,
|
||||
date_part: date_part.unwrap(),
|
||||
match_type,
|
||||
comparator,
|
||||
zone,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_timezone(&mut self) -> Result<i64, CompileError> {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
if let Token::StringConstant(value) = &token_info.token {
|
||||
let timezone = match value {
|
||||
StringConstant::String(value) => value.parse::<i64>().unwrap_or(i64::MAX),
|
||||
StringConstant::Number(Number::Integer(n)) => *n,
|
||||
StringConstant::Number(Number::Float(n)) => *n as i64,
|
||||
};
|
||||
|
||||
return match timezone {
|
||||
0..=1400 => Ok((timezone / 100 * 3600) + (timezone % 100 * 60)),
|
||||
-1200..=-1 => Ok((timezone / 100 * 3600) - (-timezone % 100 * 60)),
|
||||
_ => Err(token_info.expected("invalid timezone")),
|
||||
};
|
||||
}
|
||||
Err(token_info.expected("string containing time zone"))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
"year" => the year, "0000" .. "9999".
|
||||
"month" => the month, "01" .. "12".
|
||||
"day" => the day, "01" .. "31".
|
||||
"date" => the date in "yyyy-mm-dd" format.
|
||||
"julian" => the Modified Julian Day, that is, the date
|
||||
expressed as an integer number of days since
|
||||
00:00 UTC on November 17, 1858 (using the Gregorian
|
||||
calendar). This corresponds to the regular
|
||||
Julian Day minus 2400000.5. Sample routines to
|
||||
convert to and from modified Julian dates are
|
||||
given in Appendix A.
|
||||
"hour" => the hour, "00" .. "23".
|
||||
"minute" => the minute, "00" .. "59".
|
||||
"second" => the second, "00" .. "60".
|
||||
"time" => the time in "hh:mm:ss" format.
|
||||
"iso8601" => the date and time in restricted ISO 8601 format.
|
||||
"std11" => the date and time in a format appropriate
|
||||
for use in a Date: header field [RFC2822].
|
||||
"zone" => the time zone in use. If the user specified a
|
||||
time zone with ":zone", "zone" will
|
||||
contain that value. If :originalzone is specified
|
||||
this value will be the original zone specified
|
||||
in the date-time value. If neither argument is
|
||||
specified the value will be the server's default
|
||||
time zone in offset format "+hhmm" or "-hhmm". An
|
||||
offset of 0 (Zulu) always has a positive sign.
|
||||
"weekday" => the day of the week expressed as an integer between
|
||||
"0" and "6". "0" is Sunday, "1" is Monday, etc.
|
||||
*/
|
||||
|
||||
fn lookup_date_part(input: &str) -> Option<DatePart> {
|
||||
hashify::tiny_map!(
|
||||
input.as_bytes(),
|
||||
"year" => DatePart::Year,
|
||||
"month" => DatePart::Month,
|
||||
"day" => DatePart::Day,
|
||||
"date" => DatePart::Date,
|
||||
"julian" => DatePart::Julian,
|
||||
"hour" => DatePart::Hour,
|
||||
"minute" => DatePart::Minute,
|
||||
"second" => DatePart::Second,
|
||||
"time" => DatePart::Time,
|
||||
"iso8601" => DatePart::Iso8601,
|
||||
"std11" => DatePart::Std11,
|
||||
"zone" => DatePart::Zone,
|
||||
"weekday" => DatePart::Weekday,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Value,
|
||||
grammar::instruction::{CompilerState, MapLocalVars},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestDuplicate {
|
||||
pub handle: Option<Value>,
|
||||
pub dup_match: DupMatch,
|
||||
pub seconds: Option<u64>,
|
||||
pub last: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) enum DupMatch {
|
||||
Header(Value),
|
||||
UniqueId(Value),
|
||||
Default,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_duplicate(&mut self) -> Result<Test, CompileError> {
|
||||
let mut handle = None;
|
||||
let mut dup_match = DupMatch::Default;
|
||||
let mut seconds = None;
|
||||
let mut last = false;
|
||||
|
||||
while let Some(token_info) = self.tokens.peek() {
|
||||
let token_info = token_info?;
|
||||
let line_num = token_info.line_num;
|
||||
let line_pos = token_info.line_pos;
|
||||
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Handle) => {
|
||||
self.validate_argument(1, None, line_num, line_pos)?;
|
||||
self.tokens.next();
|
||||
handle = self.parse_string()?.into();
|
||||
}
|
||||
Token::Tag(Word::Header) => {
|
||||
self.validate_argument(2, None, line_num, line_pos)?;
|
||||
self.tokens.next();
|
||||
let header = self.parse_string()?;
|
||||
if let Value::Text(header_name) = &header
|
||||
&& HeaderName::parse(header_name.as_ref()).is_none()
|
||||
{
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::InvalidHeaderName));
|
||||
}
|
||||
dup_match = DupMatch::Header(header);
|
||||
}
|
||||
Token::Tag(Word::UniqueId) => {
|
||||
self.validate_argument(2, None, line_num, line_pos)?;
|
||||
self.tokens.next();
|
||||
dup_match = DupMatch::UniqueId(self.parse_string()?);
|
||||
}
|
||||
Token::Tag(Word::Seconds) => {
|
||||
self.validate_argument(3, None, line_num, line_pos)?;
|
||||
self.tokens.next();
|
||||
seconds = (self.tokens.expect_number(u64::MAX as usize)? as u64).into();
|
||||
}
|
||||
Token::Tag(Word::Last) => {
|
||||
self.validate_argument(4, None, line_num, line_pos)?;
|
||||
self.tokens.next();
|
||||
last = true;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Test::Duplicate(TestDuplicate {
|
||||
handle,
|
||||
dup_match,
|
||||
seconds,
|
||||
last,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl MapLocalVars for DupMatch {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
match self {
|
||||
DupMatch::Header(header) => header.map_local_vars(last_id),
|
||||
DupMatch::UniqueId(unique_id) => unique_id.map_local_vars(last_id),
|
||||
DupMatch::Default => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Envelope,
|
||||
compiler::{
|
||||
CompileError, ErrorType, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{AddressPart, MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestEnvelope {
|
||||
pub envelope_list: Vec<Envelope>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub address_part: AddressPart,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub zone: Option<i64>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_envelope(&mut self) -> Result<Test, CompileError> {
|
||||
let mut address_part = AddressPart::All;
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut envelope_list = None;
|
||||
let mut key_list;
|
||||
let mut zone = None;
|
||||
|
||||
loop {
|
||||
let mut token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::LocalPart | Word::Domain | Word::All | Word::User | Word::Detail),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
if matches!(word, Word::User | Word::Detail) {
|
||||
Capability::SubAddress.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
address_part = word.into();
|
||||
}
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex
|
||||
| Word::List),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Zone) => {
|
||||
self.validate_argument(
|
||||
4,
|
||||
Capability::EnvelopeDeliverBy.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
zone = self.parse_timezone()?.into();
|
||||
}
|
||||
_ => {
|
||||
if envelope_list.is_none() {
|
||||
let mut envelopes = Vec::new();
|
||||
let line_num = token_info.line_num;
|
||||
let line_pos = token_info.line_pos;
|
||||
|
||||
match token_info.token {
|
||||
Token::StringConstant(s) => {
|
||||
match Envelope::try_from(s.into_string().to_ascii_lowercase()) {
|
||||
Ok(envelope) => {
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
Err(invalid) => {
|
||||
token_info.token = Token::Comma;
|
||||
return Err(
|
||||
token_info.custom(ErrorType::InvalidEnvelope(invalid))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Token::BracketOpen => loop {
|
||||
let mut token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::StringConstant(s) => {
|
||||
match Envelope::try_from(
|
||||
s.into_string().to_ascii_lowercase(),
|
||||
) {
|
||||
Ok(envelope) => {
|
||||
if !envelopes.contains(&envelope) {
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
}
|
||||
Err(invalid) => {
|
||||
token_info.token = Token::Comma;
|
||||
return Err(token_info
|
||||
.custom(ErrorType::InvalidEnvelope(invalid)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Token::Comma => (),
|
||||
Token::BracketClose if !envelopes.is_empty() => break,
|
||||
_ => return Err(token_info.expected("constant string")),
|
||||
}
|
||||
},
|
||||
_ => return Err(token_info.expected("constant string")),
|
||||
}
|
||||
|
||||
for envelope in &envelopes {
|
||||
match envelope {
|
||||
Envelope::ByTimeAbsolute
|
||||
| Envelope::ByTimeRelative
|
||||
| Envelope::ByMode
|
||||
| Envelope::ByTrace => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::EnvelopeDeliverBy.into(),
|
||||
line_num,
|
||||
line_pos,
|
||||
)?;
|
||||
}
|
||||
|
||||
Envelope::Notify
|
||||
| Envelope::Orcpt
|
||||
| Envelope::Ret
|
||||
| Envelope::Envid => {
|
||||
self.validate_argument(
|
||||
0,
|
||||
Capability::EnvelopeDsn.into(),
|
||||
line_num,
|
||||
line_pos,
|
||||
)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
envelope_list = envelopes.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Envelope(TestEnvelope {
|
||||
envelope_list: envelope_list.unwrap(),
|
||||
key_list,
|
||||
address_part,
|
||||
match_type,
|
||||
comparator,
|
||||
zone,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Envelope {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if let Some(envelope) = lookup_envelope(&value) {
|
||||
Ok(envelope)
|
||||
} else {
|
||||
Err(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<&'x str> for Envelope {
|
||||
type Error = &'x str;
|
||||
|
||||
fn try_from(value: &'x str) -> Result<Self, Self::Error> {
|
||||
if let Some(envelope) = lookup_envelope(value) {
|
||||
Ok(envelope)
|
||||
} else {
|
||||
Err(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_envelope(input: &str) -> Option<Envelope> {
|
||||
hashify::tiny_map!(
|
||||
input.as_bytes(),
|
||||
"from" => Envelope::From,
|
||||
"to" => Envelope::To,
|
||||
"bytimeabsolute" => Envelope::ByTimeAbsolute,
|
||||
"bytimerelative" => Envelope::ByTimeRelative,
|
||||
"bymode" => Envelope::ByMode,
|
||||
"bytrace" => Envelope::ByTrace,
|
||||
"notify" => Envelope::Notify,
|
||||
"orcpt" => Envelope::Orcpt,
|
||||
"ret" => Envelope::Ret,
|
||||
"envid" => Envelope::Envid,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value, VariableType,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
use super::test_string::TestString;
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_environment(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut name = None;
|
||||
let mut key_list;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
if name.is_none() {
|
||||
if let Token::StringConstant(s) = token_info.token {
|
||||
name = Value::Variable(VariableType::Environment(
|
||||
s.into_string().to_lowercase(),
|
||||
))
|
||||
.into();
|
||||
} else {
|
||||
return Err(token_info.expected("environment variable"));
|
||||
}
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Environment(TestString {
|
||||
source: vec![name.unwrap()],
|
||||
key_list,
|
||||
match_type,
|
||||
comparator,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Value,
|
||||
grammar::{Capability, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestExists {
|
||||
pub header_names: Vec<Value>,
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_exists(&mut self) -> Result<Test, CompileError> {
|
||||
let mut header_names = None;
|
||||
|
||||
let mut mime = false;
|
||||
let mut mime_anychild = false;
|
||||
|
||||
while header_names.is_none() {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::AnyChild) => {
|
||||
self.validate_argument(
|
||||
2,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime_anychild = true;
|
||||
}
|
||||
_ => {
|
||||
let headers = self.parse_strings_token(token_info)?;
|
||||
for header in &headers {
|
||||
if let Value::Text(header_name) = &header
|
||||
&& HeaderName::parse(header_name.as_ref()).is_none()
|
||||
{
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::InvalidHeaderName));
|
||||
}
|
||||
}
|
||||
header_names = headers.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mime && mime_anychild {
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
|
||||
}
|
||||
|
||||
Ok(Test::Exists(TestExists {
|
||||
header_names: header_names.unwrap(),
|
||||
mime_anychild,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::CompileError;
|
||||
use crate::compiler::Value;
|
||||
use crate::compiler::grammar::instruction::CompilerState;
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestValidExtList {
|
||||
pub list_names: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_valid_ext_list(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::ValidExtList(TestValidExtList {
|
||||
list_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Value, VariableType,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, tokenizer::TokenInfo, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
/*
|
||||
Usage: hasflag [MATCH-TYPE] [COMPARATOR]
|
||||
[<variable-list: string-list>]
|
||||
<list-of-flags: string-list>
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestHasFlag {
|
||||
pub comparator: Comparator,
|
||||
pub match_type: MatchType,
|
||||
pub variable_list: Vec<VariableType>,
|
||||
pub flags: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_hasflag(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut is_local = false;
|
||||
|
||||
let mut maybe_variables;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Local) => {
|
||||
is_local = true;
|
||||
}
|
||||
_ => {
|
||||
maybe_variables = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.tokens.peek() {
|
||||
Some(Ok(TokenInfo {
|
||||
token: Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
|
||||
line_num,
|
||||
line_pos,
|
||||
})) => {
|
||||
if !maybe_variables.is_empty() {
|
||||
let line_num = *line_num;
|
||||
let line_pos = *line_pos;
|
||||
|
||||
let mut variable_list = Vec::with_capacity(maybe_variables.len());
|
||||
for variable in maybe_variables {
|
||||
match variable {
|
||||
Value::Text(var_name) => {
|
||||
variable_list.push(
|
||||
self.register_variable(var_name.to_string(), is_local)
|
||||
.map_err(|error_type| CompileError {
|
||||
line_num,
|
||||
line_pos,
|
||||
error_type,
|
||||
})?,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::ExpectedConstantString));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut flags = self.parse_strings(false)?;
|
||||
self.validate_match(&match_type, &mut flags)?;
|
||||
|
||||
Ok(Test::HasFlag(TestHasFlag {
|
||||
comparator,
|
||||
match_type,
|
||||
variable_list,
|
||||
flags,
|
||||
is_not: false,
|
||||
}))
|
||||
} else {
|
||||
Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::ExpectedConstantString))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.validate_match(&match_type, &mut maybe_variables)?;
|
||||
|
||||
Ok(Test::HasFlag(TestHasFlag {
|
||||
comparator,
|
||||
match_type,
|
||||
variable_list: Vec::new(),
|
||||
flags: maybe_variables,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, ErrorType, Value,
|
||||
grammar::{
|
||||
Capability, Comparator,
|
||||
actions::action_mime::MimeOpts,
|
||||
instruction::{CompilerState, MapLocalVars},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestHeader {
|
||||
pub header_list: Vec<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub index: Option<i32>,
|
||||
|
||||
pub mime_opts: MimeOpts<Value>,
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_header(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut header_list = None;
|
||||
let mut key_list;
|
||||
let mut index = None;
|
||||
let mut index_last = false;
|
||||
|
||||
let mut mime = false;
|
||||
let mut mime_opts = MimeOpts::None;
|
||||
let mut mime_anychild = false;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex
|
||||
| Word::List),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Index) => {
|
||||
self.validate_argument(
|
||||
3,
|
||||
Capability::Index.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
|
||||
}
|
||||
Token::Tag(Word::Last) => {
|
||||
self.validate_argument(
|
||||
4,
|
||||
Capability::Index.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
index_last = true;
|
||||
}
|
||||
Token::Tag(Word::Mime) => {
|
||||
self.validate_argument(
|
||||
5,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime = true;
|
||||
}
|
||||
Token::Tag(Word::AnyChild) => {
|
||||
self.validate_argument(
|
||||
6,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime_anychild = true;
|
||||
}
|
||||
Token::Tag(
|
||||
word @ (Word::Type | Word::Subtype | Word::ContentType | Word::Param),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
7,
|
||||
Capability::Mime.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
mime_opts = self.parse_mimeopts(word)?;
|
||||
}
|
||||
_ => {
|
||||
if header_list.is_none() {
|
||||
let headers = self.parse_strings_token(token_info)?;
|
||||
for header in &headers {
|
||||
if let Value::Text(header_name) = &header
|
||||
&& HeaderName::parse(header_name.as_ref()).is_none()
|
||||
{
|
||||
return Err(self
|
||||
.tokens
|
||||
.unwrap_next()?
|
||||
.custom(ErrorType::InvalidHeaderName));
|
||||
}
|
||||
}
|
||||
header_list = headers.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mime && (mime_anychild || mime_opts != MimeOpts::None) {
|
||||
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Header(TestHeader {
|
||||
header_list: header_list.unwrap(),
|
||||
key_list,
|
||||
match_type,
|
||||
comparator,
|
||||
index: if index_last { index.map(|i| -i) } else { index },
|
||||
mime_opts,
|
||||
mime_anychild,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl MapLocalVars for MimeOpts<Value> {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
if let MimeOpts::Param(value) = self {
|
||||
value.map_local_vars(last_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::CompileError;
|
||||
use crate::compiler::Value;
|
||||
use crate::compiler::grammar::Capability;
|
||||
use crate::compiler::grammar::instruction::{CompilerState, Instruction};
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestIhave {
|
||||
pub capabilities: Vec<Capability>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct Error {
|
||||
pub message: Value,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_ihave(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::Ihave(TestIhave {
|
||||
capabilities: self
|
||||
.parse_static_strings()?
|
||||
.into_iter()
|
||||
.map(|n| n.into())
|
||||
.collect(),
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_error(&mut self) -> Result<(), CompileError> {
|
||||
let cmd = Instruction::Error(Error {
|
||||
message: self.parse_string()?,
|
||||
});
|
||||
self.instructions.push(cmd);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Metadata,
|
||||
compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{
|
||||
Capability, Comparator,
|
||||
instruction::{CompilerState, MapLocalVars},
|
||||
},
|
||||
lexer::{Token, word::Word},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestMailboxExists {
|
||||
pub mailbox_names: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestMetadataExists {
|
||||
pub mailbox: Option<Value>,
|
||||
pub annotation_names: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
metadata [MATCH-TYPE] [COMPARATOR]
|
||||
<mailbox: string>
|
||||
<annotation-name: string> <key-list: string-list>
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestMetadata {
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub medatata: Metadata<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
servermetadata [MATCH-TYPE] [COMPARATOR]
|
||||
<annotation-name: string> <key-list: string-list>
|
||||
|
||||
*/
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_mailboxexists(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::MailboxExists(TestMailboxExists {
|
||||
mailbox_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_metadataexists(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::MetadataExists(TestMetadataExists {
|
||||
mailbox: self.parse_string()?.into(),
|
||||
annotation_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_servermetadataexists(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::MetadataExists(TestMetadataExists {
|
||||
mailbox: None,
|
||||
annotation_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_metadata(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut mailbox = None;
|
||||
let mut annotation_name = None;
|
||||
let mut key_list: Vec<Value>;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
if mailbox.is_none() {
|
||||
mailbox = self.parse_string_token(token_info)?.into();
|
||||
} else if annotation_name.is_none() {
|
||||
annotation_name = self.parse_string_token(token_info)?.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Metadata(TestMetadata {
|
||||
match_type,
|
||||
comparator,
|
||||
medatata: Metadata::Mailbox {
|
||||
name: mailbox.unwrap(),
|
||||
annotation: annotation_name.unwrap(),
|
||||
},
|
||||
key_list,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_servermetadata(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut annotation_name = None;
|
||||
let mut key_list: Vec<Value>;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
if annotation_name.is_none() {
|
||||
annotation_name = self.parse_string_token(token_info)?.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::Metadata(TestMetadata {
|
||||
match_type,
|
||||
comparator,
|
||||
medatata: Metadata::Server {
|
||||
annotation: annotation_name.unwrap(),
|
||||
},
|
||||
key_list,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl MapLocalVars for Metadata<Value> {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
match self {
|
||||
Metadata::Mailbox { name, annotation } => {
|
||||
name.map_local_vars(last_id);
|
||||
annotation.map_local_vars(last_id);
|
||||
}
|
||||
Metadata::Server { annotation } => {
|
||||
annotation.map_local_vars(last_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::CompileError;
|
||||
use crate::compiler::Value;
|
||||
use crate::compiler::grammar::instruction::CompilerState;
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestMailboxIdExists {
|
||||
pub mailbox_ids: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_mailboxidexists(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::MailboxIdExists(TestMailboxIdExists {
|
||||
mailbox_ids: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestNotifyMethodCapability {
|
||||
pub comparator: Comparator,
|
||||
pub match_type: MatchType,
|
||||
pub notification_uri: Value,
|
||||
pub notification_capability: Value,
|
||||
pub key_list: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestValidNotifyMethod {
|
||||
pub notification_uris: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_valid_notify_method(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::ValidNotifyMethod(TestValidNotifyMethod {
|
||||
notification_uris: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_notify_method_capability(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut notification_uri = None;
|
||||
let mut notification_capability = None;
|
||||
let mut key_list;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
if notification_uri.is_none() {
|
||||
notification_uri = self.parse_string_token(token_info)?.into();
|
||||
} else if notification_capability.is_none() {
|
||||
notification_capability = self.parse_string_token(token_info)?.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::NotifyMethodCapability(TestNotifyMethodCapability {
|
||||
key_list,
|
||||
match_type,
|
||||
comparator,
|
||||
notification_uri: notification_uri.unwrap(),
|
||||
notification_capability: notification_capability.unwrap(),
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError,
|
||||
grammar::instruction::CompilerState,
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestSize {
|
||||
pub over: bool,
|
||||
pub limit: usize,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_size(&mut self) -> Result<Test, CompileError> {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
let over = match token_info.token {
|
||||
Token::Tag(Word::Over) => true,
|
||||
Token::Tag(Word::Under) => false,
|
||||
_ => {
|
||||
return Err(token_info.expected("':over' or ':under'"));
|
||||
}
|
||||
};
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
if let Token::Number(limit) = token_info.token {
|
||||
Ok(Test::Size(TestSize {
|
||||
over,
|
||||
limit,
|
||||
is_not: false,
|
||||
}))
|
||||
} else {
|
||||
Err(token_info.expected("number"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
/*
|
||||
Usage: spamtest [":percent"] [COMPARATOR] [MATCH-TYPE]
|
||||
<value: string>
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestSpamTest {
|
||||
pub value: Value,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub percent: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestVirusTest {
|
||||
pub value: Value,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_spamtest(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut percent = false;
|
||||
let value;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
Token::Tag(Word::Percent) => {
|
||||
self.validate_argument(
|
||||
3,
|
||||
Capability::SpamTestPlus.into(),
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
percent = true;
|
||||
}
|
||||
_ => {
|
||||
value = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Test::SpamTest(TestSpamTest {
|
||||
value,
|
||||
percent,
|
||||
match_type,
|
||||
comparator,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_virustest(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let value;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
value = self.parse_string_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Test::VirusTest(TestVirusTest {
|
||||
value,
|
||||
match_type,
|
||||
comparator,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{CompileError, Value, grammar::instruction::CompilerState, lexer::Token};
|
||||
|
||||
use crate::compiler::grammar::test::Test;
|
||||
|
||||
/*
|
||||
Usage: specialuse_exists [<mailbox: string>]
|
||||
<special-use-attrs: string-list>
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestSpecialUseExists {
|
||||
pub mailbox: Option<Value>,
|
||||
pub attributes: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_specialuseexists(&mut self) -> Result<Test, CompileError> {
|
||||
let mut maybe_attributes = self.parse_strings(false)?;
|
||||
|
||||
match self.tokens.peek().map(|r| r.map(|t| &t.token)) {
|
||||
Some(Ok(Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen)) => {
|
||||
if maybe_attributes.len() == 1 {
|
||||
Ok(Test::SpecialUseExists(TestSpecialUseExists {
|
||||
mailbox: maybe_attributes.pop(),
|
||||
attributes: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
} else {
|
||||
Err(self.tokens.unwrap_next()?.expected("string"))
|
||||
}
|
||||
}
|
||||
_ => Ok(Test::SpecialUseExists(TestSpecialUseExists {
|
||||
mailbox: None,
|
||||
attributes: maybe_attributes,
|
||||
is_not: false,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::compiler::{
|
||||
CompileError, Value,
|
||||
grammar::{Capability, Comparator, instruction::CompilerState},
|
||||
lexer::{Token, word::Word},
|
||||
};
|
||||
|
||||
use crate::compiler::grammar::{MatchType, test::Test};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub(crate) struct TestString {
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub source: Vec<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn parse_test_string(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut source = None;
|
||||
let mut key_list: Vec<Value>;
|
||||
|
||||
loop {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::Tag(
|
||||
word @ (Word::Is
|
||||
| Word::Contains
|
||||
| Word::Matches
|
||||
| Word::Value
|
||||
| Word::Count
|
||||
| Word::Regex
|
||||
| Word::List),
|
||||
) => {
|
||||
self.validate_argument(
|
||||
1,
|
||||
match word {
|
||||
Word::Value | Word::Count => Capability::Relational.into(),
|
||||
Word::Regex => Capability::Regex.into(),
|
||||
Word::List => Capability::ExtLists.into(),
|
||||
_ => None,
|
||||
},
|
||||
token_info.line_num,
|
||||
token_info.line_pos,
|
||||
)?;
|
||||
|
||||
match_type = self.parse_match_type(word)?;
|
||||
}
|
||||
Token::Tag(Word::Comparator) => {
|
||||
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
|
||||
comparator = self.parse_comparator()?;
|
||||
}
|
||||
_ => {
|
||||
if source.is_none() {
|
||||
source = self.parse_strings_token(token_info)?.into();
|
||||
} else {
|
||||
key_list = self.parse_strings_token(token_info)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.validate_match(&match_type, &mut key_list)?;
|
||||
|
||||
Ok(Test::String(TestString {
|
||||
source: source.unwrap(),
|
||||
key_list,
|
||||
match_type,
|
||||
comparator,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod string;
|
||||
pub mod tokenizer;
|
||||
pub mod word;
|
||||
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
use self::word::Word;
|
||||
|
||||
use super::{Number, Value};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Token {
|
||||
CurlyOpen,
|
||||
CurlyClose,
|
||||
BracketOpen,
|
||||
BracketClose,
|
||||
ParenthesisOpen,
|
||||
ParenthesisClose,
|
||||
Comma,
|
||||
Semicolon,
|
||||
StringConstant(StringConstant),
|
||||
StringVariable(Vec<u8>),
|
||||
Number(usize),
|
||||
Identifier(Word),
|
||||
Tag(Word),
|
||||
Unknown(String),
|
||||
Colon,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum StringConstant {
|
||||
String(String),
|
||||
Number(Number),
|
||||
}
|
||||
|
||||
impl StringConstant {
|
||||
pub fn to_string(&'_ self) -> Cow<'_, str> {
|
||||
match self {
|
||||
StringConstant::String(s) => s.as_str().into(),
|
||||
StringConstant::Number(n) => n.to_string().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> String {
|
||||
match self {
|
||||
StringConstant::String(s) => s,
|
||||
StringConstant::Number(n) => n.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StringConstant> for Value {
|
||||
fn from(value: StringConstant) -> Self {
|
||||
match value {
|
||||
StringConstant::String(s) => Value::Text(s.into()),
|
||||
StringConstant::Number(n) => Value::Number(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Token::CurlyOpen => f.write_str("{"),
|
||||
Token::CurlyClose => f.write_str("}"),
|
||||
Token::BracketOpen => f.write_str("["),
|
||||
Token::BracketClose => f.write_str("]"),
|
||||
Token::ParenthesisOpen => f.write_str("("),
|
||||
Token::ParenthesisClose => f.write_str(")"),
|
||||
Token::Comma => f.write_str(","),
|
||||
Token::Semicolon => f.write_str(";"),
|
||||
Token::Colon => f.write_str(":"),
|
||||
Token::Number(n) => write!(f, "{n}"),
|
||||
Token::Identifier(w) => w.fmt(f),
|
||||
Token::Tag(t) => write!(f, ":{t}"),
|
||||
Token::Unknown(s) => f.write_str(s),
|
||||
Token::StringVariable(s) => f.write_str(&String::from_utf8_lossy(s)),
|
||||
Token::StringConstant(c) => match c {
|
||||
StringConstant::String(s) => f.write_str(s),
|
||||
StringConstant::Number(n) => write!(f, "{n}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
+922
@@ -0,0 +1,922 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use crate::{
|
||||
Envelope, MAX_MATCH_VARIABLES,
|
||||
compiler::{
|
||||
ContentTypePart, ErrorType, HeaderPart, HeaderVariable, MessagePart, Number,
|
||||
ReceivedHostname, ReceivedPart, Value, VariableType,
|
||||
grammar::{
|
||||
AddressPart,
|
||||
expr::{self},
|
||||
instruction::CompilerState,
|
||||
},
|
||||
},
|
||||
runtime::eval::IntoString,
|
||||
};
|
||||
|
||||
enum State {
|
||||
None,
|
||||
Variable,
|
||||
Encoded {
|
||||
is_unicode: bool,
|
||||
initial_buf_size: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl CompilerState<'_> {
|
||||
pub(crate) fn tokenize_string(
|
||||
&mut self,
|
||||
bytes: &[u8],
|
||||
parse_decoded: bool,
|
||||
) -> Result<Value, ErrorType> {
|
||||
let mut state = State::None;
|
||||
let mut items = Vec::with_capacity(3);
|
||||
let mut last_ch = 0;
|
||||
|
||||
let mut var_start_pos = usize::MAX;
|
||||
let mut var_is_number = true;
|
||||
let mut var_has_namespace = false;
|
||||
|
||||
let mut text_has_digits = true;
|
||||
let mut text_has_dots = false;
|
||||
|
||||
let mut hex_start = usize::MAX;
|
||||
let mut decode_buf = Vec::with_capacity(bytes.len());
|
||||
|
||||
for (pos, &ch) in bytes.iter().enumerate() {
|
||||
let mut is_var_error = false;
|
||||
|
||||
match state {
|
||||
State::None => match ch {
|
||||
b'{' if last_ch == b'$' => {
|
||||
decode_buf.pop();
|
||||
var_start_pos = pos + 1;
|
||||
var_is_number = true;
|
||||
var_has_namespace = false;
|
||||
state = State::Variable;
|
||||
}
|
||||
b'.' => {
|
||||
if text_has_dots {
|
||||
text_has_digits = false;
|
||||
} else {
|
||||
text_has_dots = true;
|
||||
}
|
||||
decode_buf.push(ch);
|
||||
}
|
||||
b'0'..=b'9' => {
|
||||
decode_buf.push(ch);
|
||||
}
|
||||
_ => {
|
||||
text_has_digits = false;
|
||||
decode_buf.push(ch);
|
||||
}
|
||||
},
|
||||
State::Variable => match ch {
|
||||
b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'[' | b']' | b'*' | b'-' => {
|
||||
var_is_number = false;
|
||||
}
|
||||
b'.' => {
|
||||
var_is_number = false;
|
||||
var_has_namespace = true;
|
||||
}
|
||||
b'0'..=b'9' => {}
|
||||
b'}' if pos > var_start_pos => {
|
||||
// Add any text before the variable
|
||||
if !decode_buf.is_empty() {
|
||||
self.add_value(
|
||||
&mut items,
|
||||
&decode_buf,
|
||||
parse_decoded,
|
||||
text_has_digits,
|
||||
text_has_dots,
|
||||
)?;
|
||||
decode_buf.clear();
|
||||
text_has_digits = true;
|
||||
text_has_dots = false;
|
||||
}
|
||||
|
||||
// Parse variable type
|
||||
let var_name = std::str::from_utf8(&bytes[var_start_pos..pos]).unwrap();
|
||||
let var_type = if !var_is_number {
|
||||
self.parse_variable(var_name, var_has_namespace)
|
||||
} else {
|
||||
self.parse_match_variable(var_name)
|
||||
};
|
||||
|
||||
match var_type {
|
||||
Ok(Some(var)) => items.push(Value::Variable(var)),
|
||||
Ok(None) => {}
|
||||
Err(ErrorType::InvalidNamespace(_) | ErrorType::InvalidEnvelope(_)) => {
|
||||
is_var_error = true;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
state = State::None;
|
||||
}
|
||||
b':' => {
|
||||
if parse_decoded && !var_has_namespace {
|
||||
match bytes.get(var_start_pos..pos) {
|
||||
Some(enc) if enc.eq_ignore_ascii_case(b"hex") => {
|
||||
state = State::Encoded {
|
||||
is_unicode: false,
|
||||
initial_buf_size: decode_buf.len(),
|
||||
};
|
||||
}
|
||||
Some(enc) if enc.eq_ignore_ascii_case(b"unicode") => {
|
||||
state = State::Encoded {
|
||||
is_unicode: true,
|
||||
initial_buf_size: decode_buf.len(),
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
is_var_error = true;
|
||||
}
|
||||
}
|
||||
} else if var_has_namespace {
|
||||
var_is_number = false;
|
||||
} else {
|
||||
is_var_error = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
is_var_error = true;
|
||||
}
|
||||
},
|
||||
|
||||
State::Encoded {
|
||||
is_unicode,
|
||||
initial_buf_size,
|
||||
} => match ch {
|
||||
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
|
||||
if hex_start == usize::MAX {
|
||||
hex_start = pos;
|
||||
}
|
||||
}
|
||||
b' ' | b'\t' | b'\r' | b'\n' | b'}' => {
|
||||
if hex_start != usize::MAX {
|
||||
let code = std::str::from_utf8(&bytes[hex_start..pos]).unwrap();
|
||||
hex_start = usize::MAX;
|
||||
|
||||
if !is_unicode {
|
||||
if let Ok(ch) = u8::from_str_radix(code, 16) {
|
||||
decode_buf.push(ch);
|
||||
} else {
|
||||
is_var_error = true;
|
||||
}
|
||||
} else if let Ok(ch) = u32::from_str_radix(code, 16) {
|
||||
let mut buf = [0; 4];
|
||||
decode_buf.extend_from_slice(
|
||||
char::from_u32(ch)
|
||||
.ok_or(ErrorType::InvalidUnicodeSequence(ch))?
|
||||
.encode_utf8(&mut buf)
|
||||
.as_bytes(),
|
||||
);
|
||||
} else {
|
||||
is_var_error = true;
|
||||
}
|
||||
}
|
||||
if ch == b'}' {
|
||||
if decode_buf.len() != initial_buf_size {
|
||||
state = State::None;
|
||||
} else {
|
||||
is_var_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
is_var_error = true;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if is_var_error {
|
||||
if let State::Encoded {
|
||||
initial_buf_size, ..
|
||||
} = state
|
||||
&& initial_buf_size != decode_buf.len()
|
||||
{
|
||||
decode_buf.truncate(initial_buf_size);
|
||||
}
|
||||
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..pos + 1]);
|
||||
hex_start = usize::MAX;
|
||||
state = State::None;
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
match state {
|
||||
State::Variable => {
|
||||
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
|
||||
}
|
||||
State::Encoded {
|
||||
initial_buf_size, ..
|
||||
} => {
|
||||
if initial_buf_size != decode_buf.len() {
|
||||
decode_buf.truncate(initial_buf_size);
|
||||
}
|
||||
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
|
||||
}
|
||||
State::None => (),
|
||||
}
|
||||
|
||||
if !decode_buf.is_empty() {
|
||||
self.add_value(
|
||||
&mut items,
|
||||
&decode_buf,
|
||||
parse_decoded,
|
||||
text_has_digits,
|
||||
text_has_dots,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(match items.len() {
|
||||
1 => items.pop().unwrap(),
|
||||
0 => Value::Text(String::new().into()),
|
||||
_ => Value::List(items),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_match_variable(&mut self, var_name: &str) -> Result<Option<VariableType>, ErrorType> {
|
||||
let num = var_name
|
||||
.parse()
|
||||
.map_err(|_| ErrorType::InvalidNumber(var_name.to_string()))?;
|
||||
if num < MAX_MATCH_VARIABLES as usize {
|
||||
if self.register_match_var(num) {
|
||||
let total_vars = num + 1;
|
||||
if total_vars > self.vars_match_max {
|
||||
self.vars_match_max = total_vars;
|
||||
}
|
||||
Ok(Some(VariableType::Match(num)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err(ErrorType::InvalidMatchVariable(num))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_variable(
|
||||
&self,
|
||||
var_name: &str,
|
||||
maybe_namespace: bool,
|
||||
) -> Result<Option<VariableType>, ErrorType> {
|
||||
if !maybe_namespace {
|
||||
if self.is_var_global(var_name) {
|
||||
Ok(Some(VariableType::Global(var_name.to_string())))
|
||||
} else if let Some(var_id) = self.get_local_var(var_name) {
|
||||
Ok(Some(VariableType::Local(var_id)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
let var = match var_name.to_lowercase().split_once('.') {
|
||||
Some(("global" | "t", var_name)) if !var_name.is_empty() => {
|
||||
VariableType::Global(var_name.to_string())
|
||||
}
|
||||
Some(("env", var_name)) if !var_name.is_empty() => {
|
||||
VariableType::Environment(var_name.to_string())
|
||||
}
|
||||
Some(("envelope", var_name)) if !var_name.is_empty() => {
|
||||
let envelope = match var_name {
|
||||
"from" => Envelope::From,
|
||||
"to" => Envelope::To,
|
||||
"by_time_absolute" => Envelope::ByTimeAbsolute,
|
||||
"by_time_relative" => Envelope::ByTimeRelative,
|
||||
"by_mode" => Envelope::ByMode,
|
||||
"by_trace" => Envelope::ByTrace,
|
||||
"notify" => Envelope::Notify,
|
||||
"orcpt" => Envelope::Orcpt,
|
||||
"ret" => Envelope::Ret,
|
||||
"envid" => Envelope::Envid,
|
||||
_ => {
|
||||
return Err(ErrorType::InvalidEnvelope(var_name.to_string()));
|
||||
}
|
||||
};
|
||||
VariableType::Envelope(envelope)
|
||||
}
|
||||
Some(("header", var_name)) if !var_name.is_empty() => {
|
||||
self.parse_header_variable(var_name)?
|
||||
}
|
||||
Some(("body", var_name)) if !var_name.is_empty() => match var_name {
|
||||
"text" => VariableType::Part(MessagePart::TextBody(false)),
|
||||
"html" => VariableType::Part(MessagePart::HtmlBody(false)),
|
||||
"to_text" => VariableType::Part(MessagePart::TextBody(true)),
|
||||
"to_html" => VariableType::Part(MessagePart::HtmlBody(true)),
|
||||
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
|
||||
},
|
||||
Some(("part", var_name)) if !var_name.is_empty() => match var_name {
|
||||
"text" => VariableType::Part(MessagePart::Contents),
|
||||
"raw" => VariableType::Part(MessagePart::Raw),
|
||||
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
|
||||
},
|
||||
None => {
|
||||
if self.is_var_global(var_name) {
|
||||
VariableType::Global(var_name.to_string())
|
||||
} else if let Some(var_id) = self.get_local_var(var_name) {
|
||||
VariableType::Local(var_id)
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
|
||||
};
|
||||
|
||||
Ok(Some(var))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_header_variable(&self, var_name: &str) -> Result<VariableType, ErrorType> {
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Name,
|
||||
Index,
|
||||
Part,
|
||||
PartIndex,
|
||||
}
|
||||
let mut name = vec![];
|
||||
let mut has_name = false;
|
||||
let mut has_wildcard = false;
|
||||
let mut hdr_name = String::new();
|
||||
let mut hdr_index = String::new();
|
||||
let mut part = String::new();
|
||||
let mut part_index = String::new();
|
||||
let mut state = State::Name;
|
||||
|
||||
for ch in var_name.chars() {
|
||||
match state {
|
||||
State::Name => match ch {
|
||||
'[' => {
|
||||
state = if hdr_index.is_empty() {
|
||||
State::Index
|
||||
} else if part.is_empty() {
|
||||
State::PartIndex
|
||||
} else {
|
||||
return Err(ErrorType::InvalidExpression(var_name.to_string()));
|
||||
};
|
||||
has_name = true;
|
||||
}
|
||||
'.' => {
|
||||
state = State::Part;
|
||||
has_name = true;
|
||||
}
|
||||
' ' | '\t' | '\r' | '\n' => {}
|
||||
'*' if !has_wildcard && hdr_name.is_empty() && name.is_empty() => {
|
||||
has_wildcard = true;
|
||||
}
|
||||
':' if !hdr_name.is_empty() && !has_wildcard => {
|
||||
name.push(
|
||||
HeaderName::parse(std::mem::take(&mut hdr_name)).ok_or_else(|| {
|
||||
ErrorType::InvalidExpression(var_name.to_string())
|
||||
})?,
|
||||
);
|
||||
}
|
||||
_ if !has_name && !has_wildcard => {
|
||||
hdr_name.push(ch);
|
||||
}
|
||||
_ => {
|
||||
return Err(ErrorType::InvalidExpression(var_name.to_string()));
|
||||
}
|
||||
},
|
||||
State::Index => match ch {
|
||||
']' => {
|
||||
state = State::Name;
|
||||
}
|
||||
' ' | '\t' | '\r' | '\n' => {}
|
||||
_ => {
|
||||
hdr_index.push(ch);
|
||||
}
|
||||
},
|
||||
State::Part => match ch {
|
||||
'[' => {
|
||||
state = State::PartIndex;
|
||||
}
|
||||
' ' | '\t' | '\r' | '\n' => {}
|
||||
_ => {
|
||||
part.push(ch);
|
||||
}
|
||||
},
|
||||
State::PartIndex => match ch {
|
||||
']' => {
|
||||
state = State::Name;
|
||||
}
|
||||
' ' | '\t' | '\r' | '\n' => {}
|
||||
_ => {
|
||||
part_index.push(ch);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if !hdr_name.is_empty() {
|
||||
name.push(
|
||||
HeaderName::parse(hdr_name)
|
||||
.ok_or_else(|| ErrorType::InvalidExpression(var_name.to_string()))?,
|
||||
);
|
||||
}
|
||||
|
||||
if !name.is_empty() || has_wildcard {
|
||||
Ok(VariableType::Header(HeaderVariable {
|
||||
name,
|
||||
part: HeaderPart::try_from(part.as_str())
|
||||
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
|
||||
index_hdr: match hdr_index.as_str() {
|
||||
"" => {
|
||||
if !has_wildcard {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
"*" => 0,
|
||||
_ => hdr_index
|
||||
.parse()
|
||||
.map(|v| if v == 0 { 1 } else { v })
|
||||
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
|
||||
},
|
||||
index_part: match part_index.as_str() {
|
||||
"" => {
|
||||
if !has_wildcard {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
"*" => 0,
|
||||
_ => part_index
|
||||
.parse()
|
||||
.map(|v| if v == 0 { 1 } else { v })
|
||||
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
Err(ErrorType::InvalidExpression(var_name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_expr_fnc_or_var(
|
||||
&self,
|
||||
var_name: &str,
|
||||
maybe_namespace: bool,
|
||||
) -> Result<expr::Token, String> {
|
||||
match self.parse_variable(var_name, maybe_namespace) {
|
||||
Ok(Some(var)) => Ok(expr::Token::Variable(var)),
|
||||
_ => {
|
||||
if let Some((id, num_args)) = self.compiler.functions.get(var_name) {
|
||||
Ok(expr::Token::Function {
|
||||
name: var_name.to_string(),
|
||||
id: *id,
|
||||
num_args: *num_args,
|
||||
})
|
||||
} else {
|
||||
Err(format!("Invalid variable or function name {var_name:?}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn add_value(
|
||||
&mut self,
|
||||
items: &mut Vec<Value>,
|
||||
buf: &[u8],
|
||||
parse_decoded: bool,
|
||||
has_digits: bool,
|
||||
has_dots: bool,
|
||||
) -> Result<(), ErrorType> {
|
||||
if !parse_decoded {
|
||||
items.push(if has_digits {
|
||||
if has_dots {
|
||||
match std::str::from_utf8(buf)
|
||||
.ok()
|
||||
.and_then(|v| (v, v.parse::<f64>().ok()?).into())
|
||||
{
|
||||
Some((v, n)) if n.to_string() == v => Value::Number(Number::Float(n)),
|
||||
_ => Value::Text(buf.to_vec().into_string().into()),
|
||||
}
|
||||
} else {
|
||||
match std::str::from_utf8(buf)
|
||||
.ok()
|
||||
.and_then(|v| (v, v.parse::<i64>().ok()?).into())
|
||||
{
|
||||
Some((v, n)) if n.to_string() == v => Value::Number(Number::Integer(n)),
|
||||
_ => Value::Text(buf.to_vec().into_string().into()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Value::Text(buf.to_vec().into_string().into())
|
||||
});
|
||||
} else {
|
||||
match self.tokenize_string(buf, false)? {
|
||||
Value::List(new_items) => items.extend(new_items),
|
||||
item => items.push(item),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for HeaderPart {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let (value, subvalue) = value.split_once('.').unwrap_or((value, ""));
|
||||
Ok(match value {
|
||||
"" | "text" => HeaderPart::Text,
|
||||
// Addresses
|
||||
"name" => HeaderPart::Address(AddressPart::Name),
|
||||
"addr" => {
|
||||
if !subvalue.is_empty() {
|
||||
HeaderPart::Address(AddressPart::try_from(subvalue)?)
|
||||
} else {
|
||||
HeaderPart::Address(AddressPart::All)
|
||||
}
|
||||
}
|
||||
|
||||
// Content-type
|
||||
"type" => HeaderPart::ContentType(ContentTypePart::Type),
|
||||
"subtype" => HeaderPart::ContentType(ContentTypePart::Subtype),
|
||||
"attr" if !subvalue.is_empty() => {
|
||||
HeaderPart::ContentType(ContentTypePart::Attribute(subvalue.to_string()))
|
||||
}
|
||||
|
||||
// Received
|
||||
"rcvd" => {
|
||||
if !subvalue.is_empty() {
|
||||
HeaderPart::Received(ReceivedPart::try_from(subvalue)?)
|
||||
} else {
|
||||
HeaderPart::Text
|
||||
}
|
||||
}
|
||||
|
||||
// Id
|
||||
"id" => HeaderPart::Id,
|
||||
|
||||
// Raw
|
||||
"raw" => HeaderPart::Raw,
|
||||
"raw_name" => HeaderPart::RawName,
|
||||
|
||||
// Date
|
||||
"date" => HeaderPart::Date,
|
||||
|
||||
// Exists
|
||||
"exists" => HeaderPart::Exists,
|
||||
|
||||
_ => {
|
||||
return Err(());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ReceivedPart {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
// Received
|
||||
"from" => ReceivedPart::From(ReceivedHostname::Any),
|
||||
"from.name" => ReceivedPart::From(ReceivedHostname::Name),
|
||||
"from.ip" => ReceivedPart::From(ReceivedHostname::Ip),
|
||||
"ip" => ReceivedPart::FromIp,
|
||||
"iprev" => ReceivedPart::FromIpRev,
|
||||
"by" => ReceivedPart::By(ReceivedHostname::Any),
|
||||
"by.name" => ReceivedPart::By(ReceivedHostname::Name),
|
||||
"by.ip" => ReceivedPart::By(ReceivedHostname::Ip),
|
||||
"for" => ReceivedPart::For,
|
||||
"with" => ReceivedPart::With,
|
||||
"tls" => ReceivedPart::TlsVersion,
|
||||
"cipher" => ReceivedPart::TlsCipher,
|
||||
"id" => ReceivedPart::Id,
|
||||
"ident" => ReceivedPart::Ident,
|
||||
"date" => ReceivedPart::Date,
|
||||
"date.raw" => ReceivedPart::DateRaw,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for AddressPart {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
"name" => AddressPart::Name,
|
||||
"addr" | "all" => AddressPart::All,
|
||||
"addr.domain" => AddressPart::Domain,
|
||||
"addr.local" => AddressPart::LocalPart,
|
||||
"addr.user" => AddressPart::User,
|
||||
"addr.detail" => AddressPart::Detail,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::Text(t) => f.write_str(t),
|
||||
Value::List(l) => {
|
||||
for i in l {
|
||||
i.fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Value::Number(n) => n.fmt(f),
|
||||
Value::Variable(v) => v.fmt(f),
|
||||
Value::Regex(r) => f.write_str(&r.expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for VariableType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VariableType::Local(v) => write!(f, "${{{v}}}"),
|
||||
VariableType::Match(v) => write!(f, "${{{v}}}"),
|
||||
VariableType::Global(v) => write!(f, "${{global.{v}}}"),
|
||||
VariableType::Environment(v) => write!(f, "${{env.{v}}}"),
|
||||
|
||||
VariableType::Envelope(env) => f.write_str(match env {
|
||||
Envelope::From => "${{envelope.from}}",
|
||||
Envelope::To => "${{envelope.to}}",
|
||||
Envelope::ByTimeAbsolute => "${{envelope.by_time_absolute}}",
|
||||
Envelope::ByTimeRelative => "${{envelope.by_time_relative}}",
|
||||
Envelope::ByMode => "${{envelope.by_mode}}",
|
||||
Envelope::ByTrace => "${{envelope.by_trace}}",
|
||||
Envelope::Notify => "${{envelope.notify}}",
|
||||
Envelope::Orcpt => "${{envelope.orcpt}}",
|
||||
Envelope::Ret => "${{envelope.ret}}",
|
||||
Envelope::Envid => "${{envelope.envit}}",
|
||||
}),
|
||||
|
||||
VariableType::Header(hdr) => {
|
||||
write!(
|
||||
f,
|
||||
"${{header.{}",
|
||||
hdr.name.first().map(|h| h.as_str()).unwrap_or_default()
|
||||
)?;
|
||||
if hdr.index_hdr != 0 {
|
||||
write!(f, "[{}]", hdr.index_hdr)?;
|
||||
} else {
|
||||
f.write_str("[*]")?;
|
||||
}
|
||||
/*if hdr.part != HeaderPart::Text {
|
||||
f.write_str(".")?;
|
||||
f.write_str(match &hdr.part {
|
||||
HeaderPart::Name => "name",
|
||||
HeaderPart::Address => "address",
|
||||
HeaderPart::Type => "type",
|
||||
HeaderPart::Subtype => "subtype",
|
||||
HeaderPart::Raw => "raw",
|
||||
HeaderPart::Date => "date",
|
||||
HeaderPart::Attribute(attr) => attr.as_str(),
|
||||
HeaderPart::Text => unreachable!(),
|
||||
})?;
|
||||
}*/
|
||||
if hdr.index_part != 0 {
|
||||
write!(f, "[{}]", hdr.index_part)?;
|
||||
} else {
|
||||
f.write_str("[*]")?;
|
||||
}
|
||||
f.write_str("}")
|
||||
}
|
||||
VariableType::Part(part) => {
|
||||
write!(
|
||||
f,
|
||||
"${{{}",
|
||||
match part {
|
||||
MessagePart::TextBody(true) => "body.to_text",
|
||||
MessagePart::TextBody(false) => "body.text",
|
||||
MessagePart::HtmlBody(true) => "body.to_html",
|
||||
MessagePart::HtmlBody(false) => "body.html",
|
||||
MessagePart::Contents => "part.text",
|
||||
MessagePart::Raw => "part.raw",
|
||||
}
|
||||
)?;
|
||||
f.write_str("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use mail_parser::HeaderName;
|
||||
|
||||
use super::Value;
|
||||
use crate::compiler::grammar::instruction::{Block, CompilerState, Instruction, MAX_PARAMS};
|
||||
use crate::compiler::grammar::test::Test;
|
||||
use crate::compiler::grammar::tests::test_string::TestString;
|
||||
use crate::compiler::grammar::{Comparator, MatchType};
|
||||
use crate::compiler::lexer::tokenizer::Tokenizer;
|
||||
use crate::compiler::lexer::word::Word;
|
||||
use crate::compiler::{AddressPart, HeaderPart, HeaderVariable, VariableType};
|
||||
use crate::{AHashSet, Compiler};
|
||||
|
||||
#[test]
|
||||
fn tokenize_string() {
|
||||
let c = Compiler::new();
|
||||
let mut block = Block::new(Word::Not);
|
||||
block.match_test_pos.push(0);
|
||||
let mut compiler = CompilerState {
|
||||
compiler: &c,
|
||||
instructions: vec![Instruction::Test(Test::String(TestString {
|
||||
match_type: MatchType::Regex(u64::MAX),
|
||||
comparator: Comparator::AsciiCaseMap,
|
||||
source: vec![Value::Variable(VariableType::Local(0))],
|
||||
key_list: vec![Value::Variable(VariableType::Local(0))],
|
||||
is_not: false,
|
||||
}))],
|
||||
block_stack: Vec::new(),
|
||||
block,
|
||||
last_block_type: Word::Not,
|
||||
vars_global: AHashSet::new(),
|
||||
vars_num: 0,
|
||||
vars_num_max: 0,
|
||||
vars_local: 0,
|
||||
tokens: Tokenizer::new(&c, b""),
|
||||
vars_match_max: usize::MAX,
|
||||
param_check: [false; MAX_PARAMS],
|
||||
includes_num: 0,
|
||||
};
|
||||
|
||||
for (input, expected_result) in [
|
||||
("$${hex:24 24}", Value::Text("$$$".to_string().into())),
|
||||
("$${hex:40}", Value::Text("$@".to_string().into())),
|
||||
("${hex: 40 }", Value::Text("@".to_string().into())),
|
||||
("${HEX: 40}", Value::Text("@".to_string().into())),
|
||||
("${hex:40", Value::Text("${hex:40".to_string().into())),
|
||||
("${hex:400}", Value::Text("${hex:400}".to_string().into())),
|
||||
(
|
||||
"${hex:4${hex:30}}",
|
||||
Value::Text("${hex:40}".to_string().into()),
|
||||
),
|
||||
("${unicode:40}", Value::Text("@".to_string().into())),
|
||||
(
|
||||
"${ unicode:40}",
|
||||
Value::Text("${ unicode:40}".to_string().into()),
|
||||
),
|
||||
("${UNICODE:40}", Value::Text("@".to_string().into())),
|
||||
("${UnICoDE:0000040}", Value::Text("@".to_string().into())),
|
||||
("${Unicode:40}", Value::Text("@".to_string().into())),
|
||||
(
|
||||
"${Unicode:40 40 ",
|
||||
Value::Text("${Unicode:40 40 ".to_string().into()),
|
||||
),
|
||||
(
|
||||
"${Unicode:Cool}",
|
||||
Value::Text("${Unicode:Cool}".to_string().into()),
|
||||
),
|
||||
("", Value::Text("".to_string().into())),
|
||||
(
|
||||
"${global.full}",
|
||||
Value::Variable(VariableType::Global("full".to_string())),
|
||||
),
|
||||
(
|
||||
"${BAD${global.Company}",
|
||||
Value::List(vec![
|
||||
Value::Text("${BAD".to_string().into()),
|
||||
Value::Variable(VariableType::Global("company".to_string())),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"${President, ${global.Company} Inc.}",
|
||||
Value::List(vec![
|
||||
Value::Text("${President, ".to_string().into()),
|
||||
Value::Variable(VariableType::Global("company".to_string())),
|
||||
Value::Text(" Inc.}".to_string().into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"dear${hex:20 24 7b}global.Name}",
|
||||
Value::List(vec![
|
||||
Value::Text("dear ".to_string().into()),
|
||||
Value::Variable(VariableType::Global("name".to_string())),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"INBOX.lists.${2}",
|
||||
Value::List(vec![
|
||||
Value::Text("INBOX.lists.".to_string().into()),
|
||||
Value::Variable(VariableType::Match(2)),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test",
|
||||
Value::Text("Ein unerhört großer Test".to_string().into()),
|
||||
),
|
||||
("&%${}!", Value::Text("&%${}!".to_string().into())),
|
||||
("${doh!}", Value::Text("${doh!}".to_string().into())),
|
||||
(
|
||||
"${hex: 20 }${global.hi}${hex: 20 }",
|
||||
Value::List(vec![
|
||||
Value::Text(" ".to_string().into()),
|
||||
Value::Variable(VariableType::Global("hi".to_string())),
|
||||
Value::Text(" ".to_string().into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"${hex:20 24 7b z}${global.hi}${unicode:}${unicode: }${hex:20}",
|
||||
Value::List(vec![
|
||||
Value::Text("${hex:20 24 7b z}".to_string().into()),
|
||||
Value::Variable(VariableType::Global("hi".to_string())),
|
||||
Value::Text("${unicode:}${unicode: } ".to_string().into()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"${header.from}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Text,
|
||||
index_hdr: -1,
|
||||
index_part: -1,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from.addr}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Address(AddressPart::All),
|
||||
index_hdr: -1,
|
||||
index_part: -1,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from[1]}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Text,
|
||||
index_hdr: 1,
|
||||
index_part: -1,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from[*]}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Text,
|
||||
index_hdr: 0,
|
||||
index_part: -1,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from[20].name}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Address(AddressPart::Name),
|
||||
index_hdr: 20,
|
||||
index_part: -1,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from[*].addr}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Address(AddressPart::All),
|
||||
index_hdr: 0,
|
||||
index_part: -1,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from[-5].name[2]}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Address(AddressPart::Name),
|
||||
index_hdr: -5,
|
||||
index_part: 2,
|
||||
})),
|
||||
),
|
||||
(
|
||||
"${header.from[*].raw[*]}",
|
||||
Value::Variable(VariableType::Header(HeaderVariable {
|
||||
name: vec![HeaderName::From],
|
||||
part: HeaderPart::Raw,
|
||||
index_hdr: 0,
|
||||
index_part: 0,
|
||||
})),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
compiler.tokenize_string(input.as_bytes(), true).unwrap(),
|
||||
expected_result,
|
||||
"Failed for {input}"
|
||||
);
|
||||
}
|
||||
|
||||
for input in ["${unicode:200000}", "${Unicode:DF01}"] {
|
||||
assert!(compiler.tokenize_string(input.as_bytes(), true).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
+577
@@ -0,0 +1,577 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{StringConstant, Token, word::lookup_words};
|
||||
use crate::{
|
||||
Compiler,
|
||||
compiler::{CompileError, ErrorType, Number},
|
||||
runtime::eval::IntoString,
|
||||
};
|
||||
use std::{iter::Peekable, slice::Iter};
|
||||
|
||||
pub(crate) struct Tokenizer<'x> {
|
||||
pub compiler: &'x Compiler,
|
||||
pub iter: Peekable<Iter<'x, u8>>,
|
||||
pub buf: Vec<u8>,
|
||||
pub next_token: Vec<TokenInfo>,
|
||||
|
||||
pub pos: usize,
|
||||
pub line_num: usize,
|
||||
pub line_start: usize,
|
||||
|
||||
pub text_line_num: usize,
|
||||
pub text_line_pos: usize,
|
||||
|
||||
pub token_line_num: usize,
|
||||
pub token_line_pos: usize,
|
||||
|
||||
pub token_is_tag: bool,
|
||||
|
||||
pub last_ch: u8,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TokenInfo {
|
||||
pub(crate) token: Token,
|
||||
pub(crate) line_num: usize,
|
||||
pub(crate) line_pos: usize,
|
||||
}
|
||||
|
||||
pub(crate) enum State {
|
||||
None,
|
||||
BracketComment,
|
||||
HashComment,
|
||||
QuotedString(StringType),
|
||||
MultiLine(StringType),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct StringType {
|
||||
maybe_variable: bool,
|
||||
has_other: bool,
|
||||
has_digits: bool,
|
||||
has_dots: bool,
|
||||
}
|
||||
|
||||
impl<'x> Tokenizer<'x> {
|
||||
pub fn new(compiler: &'x Compiler, bytes: &'x [u8]) -> Self {
|
||||
Tokenizer {
|
||||
compiler,
|
||||
iter: bytes.iter().peekable(),
|
||||
buf: Vec::with_capacity(bytes.len() / 2),
|
||||
pos: usize::MAX,
|
||||
line_num: 1,
|
||||
line_start: 0,
|
||||
text_line_num: 0,
|
||||
text_line_pos: 0,
|
||||
token_line_num: 0,
|
||||
token_line_pos: 0,
|
||||
token_is_tag: false,
|
||||
next_token: Vec::with_capacity(2),
|
||||
last_ch: 0,
|
||||
state: State::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_token(&mut self) -> Option<TokenInfo> {
|
||||
if !self.buf.is_empty() {
|
||||
let word = std::str::from_utf8(&self.buf).unwrap();
|
||||
let token = if let Some(word) = lookup_words(word) {
|
||||
if self.token_is_tag {
|
||||
self.token_line_pos -= 1;
|
||||
Token::Tag(word)
|
||||
} else {
|
||||
Token::Identifier(word)
|
||||
}
|
||||
} else if self.buf.first().unwrap().is_ascii_digit() {
|
||||
let multiplier = match self.buf.last().unwrap() {
|
||||
b'k' => 1024,
|
||||
b'm' => 1048576,
|
||||
b'g' => 1073741824,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
if let Ok(number) = (if multiplier > 1 && self.buf.len() > 1 {
|
||||
std::str::from_utf8(&self.buf[..self.buf.len() - 1]).unwrap()
|
||||
} else {
|
||||
word
|
||||
})
|
||||
.parse::<usize>()
|
||||
{
|
||||
Token::Number(number.saturating_mul(multiplier))
|
||||
} else if self.token_is_tag {
|
||||
Token::Unknown(format!(":{word}"))
|
||||
} else {
|
||||
Token::Unknown(word.to_string())
|
||||
}
|
||||
} else if self.token_is_tag {
|
||||
Token::Unknown(format!(":{word}"))
|
||||
} else {
|
||||
Token::Unknown(word.to_string())
|
||||
};
|
||||
|
||||
self.reset_current_token();
|
||||
|
||||
Some(TokenInfo {
|
||||
token,
|
||||
line_num: self.token_line_num,
|
||||
line_pos: self.token_line_pos,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reset_current_token(&mut self) {
|
||||
self.buf.clear();
|
||||
self.token_is_tag = false;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn token_is_tag(&mut self) {
|
||||
self.token_is_tag = true;
|
||||
}
|
||||
|
||||
pub fn get_token(&mut self, token: Token) -> TokenInfo {
|
||||
let next_token = TokenInfo {
|
||||
token,
|
||||
line_num: self.line_num,
|
||||
line_pos: self.pos - self.line_start,
|
||||
};
|
||||
if let Some(token) = self.get_current_token() {
|
||||
self.next_token.push(next_token);
|
||||
token
|
||||
} else {
|
||||
next_token
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_string(&mut self, str_type: StringType) -> Result<TokenInfo, CompileError> {
|
||||
if self.buf.len() < self.compiler.max_string_size {
|
||||
let token = if str_type.maybe_variable {
|
||||
Token::StringVariable(self.buf.to_vec())
|
||||
} else {
|
||||
let constant = self.buf.to_vec().into_string();
|
||||
if !str_type.has_other && str_type.has_digits {
|
||||
if !str_type.has_dots {
|
||||
if let Some(number) = constant
|
||||
.parse::<i64>()
|
||||
.ok()
|
||||
.filter(|&n| n.to_string() == constant)
|
||||
{
|
||||
Token::StringConstant(StringConstant::Number(Number::Integer(number)))
|
||||
} else {
|
||||
Token::StringConstant(StringConstant::String(constant))
|
||||
}
|
||||
} else if let Some(number) = constant
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|&n| n.to_string() == constant)
|
||||
{
|
||||
Token::StringConstant(StringConstant::Number(Number::Float(number)))
|
||||
} else {
|
||||
Token::StringConstant(StringConstant::String(constant))
|
||||
}
|
||||
} else {
|
||||
Token::StringConstant(StringConstant::String(constant))
|
||||
}
|
||||
};
|
||||
|
||||
self.buf.clear();
|
||||
|
||||
Ok(TokenInfo {
|
||||
token,
|
||||
line_num: self.text_line_num,
|
||||
line_pos: self.text_line_pos,
|
||||
})
|
||||
} else {
|
||||
Err(CompileError {
|
||||
line_num: self.text_line_num,
|
||||
line_pos: self.text_line_pos,
|
||||
error_type: ErrorType::StringTooLong,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn push_byte(&mut self, ch: u8) {
|
||||
if self.buf.is_empty() {
|
||||
self.token_line_num = self.line_num;
|
||||
self.token_line_pos = self.pos - self.line_start;
|
||||
}
|
||||
self.buf.push(ch);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn new_line(&mut self) {
|
||||
self.line_num += 1;
|
||||
self.line_start = self.pos;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn text_start(&mut self) {
|
||||
self.text_line_num = self.line_num;
|
||||
self.text_line_pos = self.pos - self.line_start;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_token_start(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn token_bytes(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn next_byte(&mut self) -> Option<(u8, u8)> {
|
||||
self.iter.next().map(|&ch| {
|
||||
let last_ch = self.last_ch;
|
||||
self.pos = self.pos.wrapping_add(1);
|
||||
self.last_ch = ch;
|
||||
(ch, last_ch)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn peek_byte(&mut self) -> Option<u8> {
|
||||
self.iter.peek().map(|ch| **ch)
|
||||
}
|
||||
|
||||
pub fn unwrap_next(&mut self) -> Result<TokenInfo, CompileError> {
|
||||
if let Some(token) = self.next() {
|
||||
token
|
||||
} else {
|
||||
Err(CompileError {
|
||||
line_num: self.line_num,
|
||||
line_pos: self.pos - self.line_start,
|
||||
error_type: ErrorType::UnexpectedEOF,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_token(&mut self, token: Token) -> Result<(), CompileError> {
|
||||
let next_token = self.unwrap_next()?;
|
||||
if next_token.token == token {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(next_token.expected(format!("'{token}'")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_static_string(&mut self) -> Result<String, CompileError> {
|
||||
let next_token = self.unwrap_next()?;
|
||||
match next_token.token {
|
||||
Token::StringConstant(s) => Ok(s.into_string()),
|
||||
Token::BracketOpen => {
|
||||
let mut string = None;
|
||||
loop {
|
||||
let token_info = self.unwrap_next()?;
|
||||
match token_info.token {
|
||||
Token::StringConstant(string_) => {
|
||||
string = string_.into();
|
||||
}
|
||||
Token::BracketClose if string.is_some() => break,
|
||||
_ => return Err(token_info.expected("constant string")),
|
||||
}
|
||||
}
|
||||
Ok(string.unwrap().into_string())
|
||||
}
|
||||
_ => Err(next_token.expected("constant string")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_number(&mut self, max_value: usize) -> Result<usize, CompileError> {
|
||||
let next_token = self.unwrap_next()?;
|
||||
if let Token::Number(n) = next_token.token {
|
||||
if n < max_value {
|
||||
Ok(n)
|
||||
} else {
|
||||
Err(next_token.expected(format!("number lower than {max_value}")))
|
||||
}
|
||||
} else {
|
||||
Err(next_token.expected("number"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_character(&self) -> CompileError {
|
||||
CompileError {
|
||||
line_num: self.line_num,
|
||||
line_pos: self.pos - self.line_start,
|
||||
error_type: ErrorType::InvalidCharacter(self.last_ch),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(&mut self) -> Option<Result<&TokenInfo, CompileError>> {
|
||||
if self.next_token.is_empty() {
|
||||
match self.next()? {
|
||||
Ok(next_token) => self.next_token.push(next_token),
|
||||
Err(err) => return Some(Err(err)),
|
||||
}
|
||||
}
|
||||
self.next_token.last().map(Ok)
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Tokenizer<'_> {
|
||||
type Item = Result<TokenInfo, CompileError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(prev_token) = self.next_token.pop() {
|
||||
return Some(Ok(prev_token));
|
||||
}
|
||||
|
||||
'outer: while let Some((ch, last_ch)) = self.next_byte() {
|
||||
match self.state {
|
||||
State::None => match ch {
|
||||
b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'$' => {
|
||||
self.push_byte(ch);
|
||||
}
|
||||
b'A'..=b'Z' => {
|
||||
self.push_byte(ch.to_ascii_lowercase());
|
||||
}
|
||||
b':' => {
|
||||
if self.is_token_start()
|
||||
&& matches!(self.peek_byte(), Some(b) if b.is_ascii_alphabetic())
|
||||
{
|
||||
self.token_is_tag();
|
||||
} else if self.token_bytes().eq_ignore_ascii_case(b"text") {
|
||||
self.state = State::MultiLine(StringType::default());
|
||||
self.text_start();
|
||||
while let Some((ch, _)) = self.next_byte() {
|
||||
if ch == b'\n' {
|
||||
self.new_line();
|
||||
self.reset_current_token();
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Some(Ok(self.get_token(Token::Colon)));
|
||||
//return Some(Err(self.invalid_character()));
|
||||
}
|
||||
}
|
||||
b'"' => {
|
||||
self.state = State::QuotedString(StringType::default());
|
||||
self.text_start();
|
||||
if let Some(token) = self.get_current_token() {
|
||||
return Some(Ok(token));
|
||||
}
|
||||
}
|
||||
b'{' => {
|
||||
return Some(Ok(self.get_token(Token::CurlyOpen)));
|
||||
}
|
||||
b'}' => {
|
||||
return Some(Ok(self.get_token(Token::CurlyClose)));
|
||||
}
|
||||
b';' => {
|
||||
return Some(Ok(self.get_token(Token::Semicolon)));
|
||||
}
|
||||
b',' => {
|
||||
return Some(Ok(self.get_token(Token::Comma)));
|
||||
}
|
||||
b'[' => {
|
||||
return Some(Ok(self.get_token(Token::BracketOpen)));
|
||||
}
|
||||
b']' => {
|
||||
return Some(Ok(self.get_token(Token::BracketClose)));
|
||||
}
|
||||
b'(' => {
|
||||
return Some(Ok(self.get_token(Token::ParenthesisOpen)));
|
||||
}
|
||||
b')' => {
|
||||
return Some(Ok(self.get_token(Token::ParenthesisClose)));
|
||||
}
|
||||
b'/' => {
|
||||
if let Some((b'*', _)) = self.next_byte() {
|
||||
self.last_ch = 0;
|
||||
self.state = State::BracketComment;
|
||||
self.text_start();
|
||||
if let Some(token) = self.get_current_token() {
|
||||
return Some(Ok(token));
|
||||
}
|
||||
} else {
|
||||
return Some(Err(self.invalid_character()));
|
||||
}
|
||||
}
|
||||
b'#' => {
|
||||
self.state = State::HashComment;
|
||||
if let Some(token) = self.get_current_token() {
|
||||
return Some(Ok(token));
|
||||
}
|
||||
}
|
||||
b'\n' => {
|
||||
self.new_line();
|
||||
if let Some(token) = self.get_current_token() {
|
||||
return Some(Ok(token));
|
||||
}
|
||||
}
|
||||
b' ' | b'\t' | b'\r' => {
|
||||
if let Some(token) = self.get_current_token() {
|
||||
return Some(Ok(token));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Some(Err(self.invalid_character()));
|
||||
}
|
||||
},
|
||||
State::BracketComment => match ch {
|
||||
b'/' if last_ch == b'*' => {
|
||||
self.state = State::None;
|
||||
}
|
||||
b'\n' => {
|
||||
self.new_line();
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
State::HashComment => {
|
||||
if ch == b'\n' {
|
||||
self.state = State::None;
|
||||
self.new_line();
|
||||
}
|
||||
}
|
||||
State::QuotedString(mut str_type) => match ch {
|
||||
b'"' if last_ch != b'\\' => {
|
||||
self.state = State::None;
|
||||
return Some(self.get_string(str_type));
|
||||
}
|
||||
b'\n' => {
|
||||
self.new_line();
|
||||
self.push_byte(b'\n');
|
||||
str_type.has_other = true;
|
||||
self.state = State::QuotedString(str_type);
|
||||
}
|
||||
b'{' if (last_ch == b'$' || last_ch == b'%') => {
|
||||
str_type.maybe_variable = true;
|
||||
self.state = State::QuotedString(str_type);
|
||||
self.push_byte(ch);
|
||||
}
|
||||
b'\\' => {
|
||||
if last_ch == b'\\' {
|
||||
self.push_byte(ch);
|
||||
}
|
||||
}
|
||||
b'0'..=b'9' => {
|
||||
if !str_type.has_digits {
|
||||
str_type.has_digits = true;
|
||||
self.state = State::QuotedString(str_type);
|
||||
}
|
||||
self.push_byte(ch);
|
||||
}
|
||||
b'.' => {
|
||||
if !str_type.has_dots {
|
||||
str_type.has_dots = true;
|
||||
} else {
|
||||
str_type.has_other = true;
|
||||
}
|
||||
self.state = State::QuotedString(str_type);
|
||||
self.push_byte(ch);
|
||||
}
|
||||
_ => {
|
||||
let ch = if last_ch == b'\\' {
|
||||
match ch {
|
||||
b'n' => b'\n',
|
||||
b'r' => b'\r',
|
||||
b't' => b'\t',
|
||||
_ => ch,
|
||||
}
|
||||
} else {
|
||||
ch
|
||||
};
|
||||
if !str_type.has_other && ch != b'-' {
|
||||
str_type.has_other = true;
|
||||
self.state = State::QuotedString(str_type);
|
||||
}
|
||||
self.push_byte(ch);
|
||||
}
|
||||
},
|
||||
State::MultiLine(mut str_type) => match ch {
|
||||
b'.' if last_ch == b'\n' => {
|
||||
let is_eof = match (self.next_byte(), self.peek_byte()) {
|
||||
(Some((b'\r', _)), Some(b'\n')) => {
|
||||
self.next_byte();
|
||||
true
|
||||
}
|
||||
(Some((b'\n', _)), _) => true,
|
||||
(Some((b'.', _)), _) => {
|
||||
self.push_byte(b'.');
|
||||
false
|
||||
}
|
||||
(Some((ch, _)), _) => {
|
||||
self.push_byte(b'.');
|
||||
self.push_byte(ch);
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_eof {
|
||||
self.new_line();
|
||||
self.state = State::None;
|
||||
return Some(self.get_string(str_type));
|
||||
}
|
||||
}
|
||||
b'\n' => {
|
||||
self.new_line();
|
||||
self.push_byte(b'\n');
|
||||
}
|
||||
b'{' if (last_ch == b'$' || last_ch == b'%') => {
|
||||
str_type.maybe_variable = true;
|
||||
self.state = State::MultiLine(str_type);
|
||||
self.push_byte(ch);
|
||||
}
|
||||
b'0'..=b'9' => {
|
||||
if !str_type.has_digits {
|
||||
str_type.has_digits = true;
|
||||
self.state = State::MultiLine(str_type);
|
||||
}
|
||||
self.push_byte(ch);
|
||||
}
|
||||
b'.' => {
|
||||
if !str_type.has_dots {
|
||||
str_type.has_dots = true;
|
||||
} else {
|
||||
str_type.has_other = true;
|
||||
}
|
||||
self.state = State::MultiLine(str_type);
|
||||
self.push_byte(ch);
|
||||
}
|
||||
_ => {
|
||||
if !str_type.has_other && ch != b'-' {
|
||||
str_type.has_other = true;
|
||||
self.state = State::MultiLine(str_type);
|
||||
}
|
||||
self.push_byte(ch);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
match self.state {
|
||||
State::BracketComment | State::QuotedString(_) | State::MultiLine(_) => {
|
||||
Some(Err(CompileError {
|
||||
line_num: self.text_line_num,
|
||||
line_pos: self.text_line_pos,
|
||||
error_type: (&self.state).into(),
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&State> for ErrorType {
|
||||
fn from(state: &State) -> Self {
|
||||
match state {
|
||||
State::BracketComment => ErrorType::UnterminatedComment,
|
||||
State::QuotedString(_) => ErrorType::UnterminatedString,
|
||||
State::MultiLine(_) => ErrorType::UnterminatedMultiline,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub(crate) enum Word {
|
||||
AddFlag,
|
||||
AddHeader,
|
||||
Address,
|
||||
Addresses,
|
||||
All,
|
||||
AllOf,
|
||||
AnyChild,
|
||||
AnyOf,
|
||||
Body,
|
||||
Break,
|
||||
ByMode,
|
||||
ByTimeAbsolute,
|
||||
ByTimeRelative,
|
||||
ByTrace,
|
||||
Comparator,
|
||||
Contains,
|
||||
Content,
|
||||
ContentType,
|
||||
Convert,
|
||||
Copy,
|
||||
Count,
|
||||
Create,
|
||||
CurrentDate,
|
||||
Date,
|
||||
Days,
|
||||
DeleteHeader,
|
||||
Detail,
|
||||
Discard,
|
||||
Domain,
|
||||
Duplicate,
|
||||
Else,
|
||||
ElsIf,
|
||||
Enclose,
|
||||
EncodeUrl,
|
||||
Envelope,
|
||||
Environment,
|
||||
Ereject,
|
||||
Error,
|
||||
Exists,
|
||||
ExtractText,
|
||||
False,
|
||||
Fcc,
|
||||
FileInto,
|
||||
First,
|
||||
Flags,
|
||||
ForEveryPart,
|
||||
From,
|
||||
Global,
|
||||
Handle,
|
||||
HasFlag,
|
||||
Header,
|
||||
Headers,
|
||||
If,
|
||||
Ihave,
|
||||
Importance,
|
||||
Include,
|
||||
Index,
|
||||
Is,
|
||||
Keep,
|
||||
Last,
|
||||
Length,
|
||||
List,
|
||||
LocalPart,
|
||||
Lower,
|
||||
LowerFirst,
|
||||
MailboxExists,
|
||||
MailboxId,
|
||||
MailboxIdExists,
|
||||
Matches,
|
||||
Message,
|
||||
Metadata,
|
||||
MetadataExists,
|
||||
Mime,
|
||||
Name,
|
||||
Not,
|
||||
Notify,
|
||||
NotifyMethodCapability,
|
||||
Once,
|
||||
Optional,
|
||||
Options,
|
||||
OriginalZone,
|
||||
Over,
|
||||
Param,
|
||||
Percent,
|
||||
Personal,
|
||||
QuoteRegex,
|
||||
QuoteWildcard,
|
||||
Raw,
|
||||
Redirect,
|
||||
Regex,
|
||||
Reject,
|
||||
RemoveFlag,
|
||||
Replace,
|
||||
Require,
|
||||
Ret,
|
||||
Return,
|
||||
Seconds,
|
||||
ServerMetadata,
|
||||
ServerMetadataExists,
|
||||
Set,
|
||||
SetFlag,
|
||||
Size,
|
||||
SpamTest,
|
||||
SpecialUse,
|
||||
SpecialUseExists,
|
||||
Stop,
|
||||
String,
|
||||
Subject,
|
||||
Subtype,
|
||||
Text,
|
||||
True,
|
||||
Type,
|
||||
Under,
|
||||
UniqueId,
|
||||
Upper,
|
||||
UpperFirst,
|
||||
User,
|
||||
Vacation,
|
||||
ValidExtList,
|
||||
ValidNotifyMethod,
|
||||
Value,
|
||||
VirusTest,
|
||||
Zone,
|
||||
|
||||
// Extensions
|
||||
Eval,
|
||||
Local,
|
||||
While,
|
||||
Let,
|
||||
Continue,
|
||||
}
|
||||
|
||||
pub(crate) fn lookup_words(input: &str) -> Option<Word> {
|
||||
hashify::tiny_map!(
|
||||
input.as_bytes(),
|
||||
"addflag" => Word::AddFlag,
|
||||
"addheader" => Word::AddHeader,
|
||||
"address" => Word::Address,
|
||||
"addresses" => Word::Addresses,
|
||||
"all" => Word::All,
|
||||
"allof" => Word::AllOf,
|
||||
"anychild" => Word::AnyChild,
|
||||
"anyof" => Word::AnyOf,
|
||||
"body" => Word::Body,
|
||||
"break" => Word::Break,
|
||||
"bymode" => Word::ByMode,
|
||||
"bytimeabsolute" => Word::ByTimeAbsolute,
|
||||
"bytimerelative" => Word::ByTimeRelative,
|
||||
"bytrace" => Word::ByTrace,
|
||||
"comparator" => Word::Comparator,
|
||||
"contains" => Word::Contains,
|
||||
"content" => Word::Content,
|
||||
"contenttype" => Word::ContentType,
|
||||
"convert" => Word::Convert,
|
||||
"copy" => Word::Copy,
|
||||
"count" => Word::Count,
|
||||
"create" => Word::Create,
|
||||
"currentdate" => Word::CurrentDate,
|
||||
"date" => Word::Date,
|
||||
"days" => Word::Days,
|
||||
"deleteheader" => Word::DeleteHeader,
|
||||
"detail" => Word::Detail,
|
||||
"discard" => Word::Discard,
|
||||
"domain" => Word::Domain,
|
||||
"duplicate" => Word::Duplicate,
|
||||
"else" => Word::Else,
|
||||
"elsif" => Word::ElsIf,
|
||||
"enclose" => Word::Enclose,
|
||||
"encodeurl" => Word::EncodeUrl,
|
||||
"envelope" => Word::Envelope,
|
||||
"environment" => Word::Environment,
|
||||
"ereject" => Word::Ereject,
|
||||
"error" => Word::Error,
|
||||
"exists" => Word::Exists,
|
||||
"extracttext" => Word::ExtractText,
|
||||
"false" => Word::False,
|
||||
"fcc" => Word::Fcc,
|
||||
"fileinto" => Word::FileInto,
|
||||
"first" => Word::First,
|
||||
"flags" => Word::Flags,
|
||||
"foreverypart" => Word::ForEveryPart,
|
||||
"from" => Word::From,
|
||||
"global" => Word::Global,
|
||||
"handle" => Word::Handle,
|
||||
"hasflag" => Word::HasFlag,
|
||||
"header" => Word::Header,
|
||||
"headers" => Word::Headers,
|
||||
"if" => Word::If,
|
||||
"ihave" => Word::Ihave,
|
||||
"importance" => Word::Importance,
|
||||
"include" => Word::Include,
|
||||
"index" => Word::Index,
|
||||
"is" => Word::Is,
|
||||
"keep" => Word::Keep,
|
||||
"last" => Word::Last,
|
||||
"length" => Word::Length,
|
||||
"list" => Word::List,
|
||||
"localpart" => Word::LocalPart,
|
||||
"lower" => Word::Lower,
|
||||
"lowerfirst" => Word::LowerFirst,
|
||||
"mailboxexists" => Word::MailboxExists,
|
||||
"mailboxid" => Word::MailboxId,
|
||||
"mailboxidexists" => Word::MailboxIdExists,
|
||||
"matches" => Word::Matches,
|
||||
"message" => Word::Message,
|
||||
"metadata" => Word::Metadata,
|
||||
"metadataexists" => Word::MetadataExists,
|
||||
"mime" => Word::Mime,
|
||||
"name" => Word::Name,
|
||||
"not" => Word::Not,
|
||||
"notify" => Word::Notify,
|
||||
"notify_method_capability" => Word::NotifyMethodCapability,
|
||||
"once" => Word::Once,
|
||||
"optional" => Word::Optional,
|
||||
"options" => Word::Options,
|
||||
"originalzone" => Word::OriginalZone,
|
||||
"over" => Word::Over,
|
||||
"param" => Word::Param,
|
||||
"percent" => Word::Percent,
|
||||
"personal" => Word::Personal,
|
||||
"quoteregex" => Word::QuoteRegex,
|
||||
"quotewildcard" => Word::QuoteWildcard,
|
||||
"raw" => Word::Raw,
|
||||
"redirect" => Word::Redirect,
|
||||
"regex" => Word::Regex,
|
||||
"reject" => Word::Reject,
|
||||
"removeflag" => Word::RemoveFlag,
|
||||
"replace" => Word::Replace,
|
||||
"require" => Word::Require,
|
||||
"ret" => Word::Ret,
|
||||
"return" => Word::Return,
|
||||
"seconds" => Word::Seconds,
|
||||
"servermetadata" => Word::ServerMetadata,
|
||||
"servermetadataexists" => Word::ServerMetadataExists,
|
||||
"set" => Word::Set,
|
||||
"setflag" => Word::SetFlag,
|
||||
"size" => Word::Size,
|
||||
"spamtest" => Word::SpamTest,
|
||||
"specialuse" => Word::SpecialUse,
|
||||
"specialuse_exists" => Word::SpecialUseExists,
|
||||
"stop" => Word::Stop,
|
||||
"string" => Word::String,
|
||||
"subject" => Word::Subject,
|
||||
"subtype" => Word::Subtype,
|
||||
"text" => Word::Text,
|
||||
"true" => Word::True,
|
||||
"type" => Word::Type,
|
||||
"under" => Word::Under,
|
||||
"uniqueid" => Word::UniqueId,
|
||||
"upper" => Word::Upper,
|
||||
"upperfirst" => Word::UpperFirst,
|
||||
"user" => Word::User,
|
||||
"vacation" => Word::Vacation,
|
||||
"valid_ext_list" => Word::ValidExtList,
|
||||
"valid_notify_method" => Word::ValidNotifyMethod,
|
||||
"value" => Word::Value,
|
||||
"virustest" => Word::VirusTest,
|
||||
"zone" => Word::Zone,
|
||||
"eval" => Word::Eval,
|
||||
"local" => Word::Local,
|
||||
"while" => Word::While,
|
||||
"let" => Word::Let,
|
||||
"continue" => Word::Continue,
|
||||
)
|
||||
}
|
||||
|
||||
impl Display for Word {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Word::AddFlag => f.write_str("addflag"),
|
||||
Word::AddHeader => f.write_str("addheader"),
|
||||
Word::Address => f.write_str("address"),
|
||||
Word::Addresses => f.write_str("addresses"),
|
||||
Word::All => f.write_str("all"),
|
||||
Word::AllOf => f.write_str("allof"),
|
||||
Word::AnyChild => f.write_str("anychild"),
|
||||
Word::AnyOf => f.write_str("anyof"),
|
||||
Word::Body => f.write_str("body"),
|
||||
Word::Break => f.write_str("break"),
|
||||
Word::ByMode => f.write_str("bymode"),
|
||||
Word::ByTimeAbsolute => f.write_str("bytimeabsolute"),
|
||||
Word::ByTimeRelative => f.write_str("bytimerelative"),
|
||||
Word::ByTrace => f.write_str("bytrace"),
|
||||
Word::Comparator => f.write_str("comparator"),
|
||||
Word::Contains => f.write_str("contains"),
|
||||
Word::Content => f.write_str("content"),
|
||||
Word::ContentType => f.write_str("contenttype"),
|
||||
Word::Convert => f.write_str("convert"),
|
||||
Word::Copy => f.write_str("copy"),
|
||||
Word::Count => f.write_str("count"),
|
||||
Word::Create => f.write_str("create"),
|
||||
Word::CurrentDate => f.write_str("currentdate"),
|
||||
Word::Date => f.write_str("date"),
|
||||
Word::Days => f.write_str("days"),
|
||||
Word::DeleteHeader => f.write_str("deleteheader"),
|
||||
Word::Detail => f.write_str("detail"),
|
||||
Word::Discard => f.write_str("discard"),
|
||||
Word::Domain => f.write_str("domain"),
|
||||
Word::Duplicate => f.write_str("duplicate"),
|
||||
Word::Else => f.write_str("else"),
|
||||
Word::ElsIf => f.write_str("elsif"),
|
||||
Word::Enclose => f.write_str("enclose"),
|
||||
Word::EncodeUrl => f.write_str("encodeurl"),
|
||||
Word::Envelope => f.write_str("envelope"),
|
||||
Word::Environment => f.write_str("environment"),
|
||||
Word::Ereject => f.write_str("ereject"),
|
||||
Word::Error => f.write_str("error"),
|
||||
Word::Exists => f.write_str("exists"),
|
||||
Word::ExtractText => f.write_str("extracttext"),
|
||||
Word::False => f.write_str("false"),
|
||||
Word::Fcc => f.write_str("fcc"),
|
||||
Word::FileInto => f.write_str("fileinto"),
|
||||
Word::First => f.write_str("first"),
|
||||
Word::Flags => f.write_str("flags"),
|
||||
Word::ForEveryPart => f.write_str("foreverypart"),
|
||||
Word::From => f.write_str("from"),
|
||||
Word::Global => f.write_str("global"),
|
||||
Word::Handle => f.write_str("handle"),
|
||||
Word::HasFlag => f.write_str("hasflag"),
|
||||
Word::Header => f.write_str("header"),
|
||||
Word::Headers => f.write_str("headers"),
|
||||
Word::If => f.write_str("if"),
|
||||
Word::Ihave => f.write_str("ihave"),
|
||||
Word::Importance => f.write_str("importance"),
|
||||
Word::Include => f.write_str("include"),
|
||||
Word::Index => f.write_str("index"),
|
||||
Word::Is => f.write_str("is"),
|
||||
Word::Keep => f.write_str("keep"),
|
||||
Word::Last => f.write_str("last"),
|
||||
Word::Length => f.write_str("length"),
|
||||
Word::List => f.write_str("list"),
|
||||
Word::LocalPart => f.write_str("localpart"),
|
||||
Word::Lower => f.write_str("lower"),
|
||||
Word::LowerFirst => f.write_str("lowerfirst"),
|
||||
Word::MailboxExists => f.write_str("mailboxexists"),
|
||||
Word::MailboxId => f.write_str("mailboxid"),
|
||||
Word::MailboxIdExists => f.write_str("mailboxidexists"),
|
||||
Word::Matches => f.write_str("matches"),
|
||||
Word::Message => f.write_str("message"),
|
||||
Word::Metadata => f.write_str("metadata"),
|
||||
Word::MetadataExists => f.write_str("metadataexists"),
|
||||
Word::Mime => f.write_str("mime"),
|
||||
Word::Name => f.write_str("name"),
|
||||
Word::Not => f.write_str("not"),
|
||||
Word::Notify => f.write_str("notify"),
|
||||
Word::NotifyMethodCapability => f.write_str("notify_method_capability"),
|
||||
Word::Once => f.write_str("once"),
|
||||
Word::Optional => f.write_str("optional"),
|
||||
Word::Options => f.write_str("options"),
|
||||
Word::OriginalZone => f.write_str("originalzone"),
|
||||
Word::Over => f.write_str("over"),
|
||||
Word::Param => f.write_str("param"),
|
||||
Word::Percent => f.write_str("percent"),
|
||||
Word::Personal => f.write_str("personal"),
|
||||
Word::QuoteRegex => f.write_str("quoteregex"),
|
||||
Word::QuoteWildcard => f.write_str("quotewildcard"),
|
||||
Word::Raw => f.write_str("raw"),
|
||||
Word::Redirect => f.write_str("redirect"),
|
||||
Word::Regex => f.write_str("regex"),
|
||||
Word::Reject => f.write_str("reject"),
|
||||
Word::RemoveFlag => f.write_str("removeflag"),
|
||||
Word::Replace => f.write_str("replace"),
|
||||
Word::Require => f.write_str("require"),
|
||||
Word::Ret => f.write_str("ret"),
|
||||
Word::Return => f.write_str("return"),
|
||||
Word::Seconds => f.write_str("seconds"),
|
||||
Word::ServerMetadata => f.write_str("servermetadata"),
|
||||
Word::ServerMetadataExists => f.write_str("servermetadataexists"),
|
||||
Word::Set => f.write_str("set"),
|
||||
Word::SetFlag => f.write_str("setflag"),
|
||||
Word::Size => f.write_str("size"),
|
||||
Word::SpamTest => f.write_str("spamtest"),
|
||||
Word::SpecialUse => f.write_str("specialuse"),
|
||||
Word::SpecialUseExists => f.write_str("specialuse_exists"),
|
||||
Word::Stop => f.write_str("stop"),
|
||||
Word::String => f.write_str("string"),
|
||||
Word::Subject => f.write_str("subject"),
|
||||
Word::Subtype => f.write_str("subtype"),
|
||||
Word::Text => f.write_str("text"),
|
||||
Word::True => f.write_str("true"),
|
||||
Word::Type => f.write_str("type"),
|
||||
Word::Under => f.write_str("under"),
|
||||
Word::UniqueId => f.write_str("uniqueid"),
|
||||
Word::Upper => f.write_str("upper"),
|
||||
Word::UpperFirst => f.write_str("upperfirst"),
|
||||
Word::User => f.write_str("user"),
|
||||
Word::Vacation => f.write_str("vacation"),
|
||||
Word::ValidExtList => f.write_str("valid_ext_list"),
|
||||
Word::ValidNotifyMethod => f.write_str("valid_notify_method"),
|
||||
Word::Value => f.write_str("value"),
|
||||
Word::VirusTest => f.write_str("virustest"),
|
||||
Word::Zone => f.write_str("zone"),
|
||||
Word::Eval => f.write_str("eval"),
|
||||
Word::Local => f.write_str("local"),
|
||||
Word::While => f.write_str("while"),
|
||||
Word::Let => f.write_str("let"),
|
||||
Word::Continue => f.write_str("continue"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+662
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use self::{
|
||||
grammar::{AddressPart, Capability},
|
||||
lexer::tokenizer::TokenInfo,
|
||||
};
|
||||
use crate::{Compiler, Envelope, FunctionMap, runtime::RuntimeError};
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use mail_parser::HeaderName;
|
||||
use std::{borrow::Cow, fmt::Display, sync::Arc};
|
||||
|
||||
pub mod grammar;
|
||||
pub mod lexer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CompileError {
|
||||
line_num: usize,
|
||||
line_pos: usize,
|
||||
error_type: ErrorType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorType {
|
||||
InvalidCharacter(u8),
|
||||
InvalidNumber(String),
|
||||
InvalidMatchVariable(usize),
|
||||
InvalidUnicodeSequence(u32),
|
||||
InvalidNamespace(String),
|
||||
InvalidRegex(String),
|
||||
InvalidExpression(String),
|
||||
InvalidUtf8String,
|
||||
InvalidHeaderName,
|
||||
InvalidArguments,
|
||||
InvalidAddress,
|
||||
InvalidURI,
|
||||
InvalidEnvelope(String),
|
||||
UnterminatedString,
|
||||
UnterminatedComment,
|
||||
UnterminatedMultiline,
|
||||
UnterminatedBlock,
|
||||
ScriptTooLong,
|
||||
StringTooLong,
|
||||
VariableTooLong,
|
||||
VariableIsLocal(String),
|
||||
HeaderTooLong,
|
||||
ExpectedConstantString,
|
||||
UnexpectedToken {
|
||||
expected: Cow<'static, str>,
|
||||
found: String,
|
||||
},
|
||||
UnexpectedEOF,
|
||||
TooManyNestedBlocks,
|
||||
TooManyNestedTests,
|
||||
TooManyNestedForEveryParts,
|
||||
TooManyIncludes,
|
||||
LabelAlreadyDefined(String),
|
||||
LabelUndefined(String),
|
||||
BreakOutsideLoop,
|
||||
ContinueOutsideLoop,
|
||||
UnsupportedComparator(String),
|
||||
DuplicatedParameter,
|
||||
UndeclaredCapability(Capability),
|
||||
MissingTag(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl Default for Compiler {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
rkyv(serialize_bounds(
|
||||
__S: rkyv::ser::Writer + rkyv::ser::Allocator,
|
||||
__S::Error: rkyv::rancor::Source,
|
||||
))
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source))
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
rkyv(bytecheck(
|
||||
bounds(
|
||||
__C: rkyv::validation::ArchiveContext,
|
||||
)
|
||||
))
|
||||
)]
|
||||
pub(crate) enum Value {
|
||||
Text(Arc<String>),
|
||||
Number(Number),
|
||||
Variable(VariableType),
|
||||
Regex(Regex),
|
||||
List(#[cfg_attr(feature = "rkyv", rkyv(omit_bounds))] Vec<Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
pub struct Regex {
|
||||
#[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))]
|
||||
#[cfg_attr(any(test, feature = "serde"), serde(skip, default))]
|
||||
pub regex: LazyRegex,
|
||||
pub expr: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LazyRegex(pub Arc<ArcSwap<Option<fancy_regex::Regex>>>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum VariableType {
|
||||
Local(usize),
|
||||
Match(usize),
|
||||
Global(String),
|
||||
Environment(String),
|
||||
Envelope(Envelope),
|
||||
Header(HeaderVariable<'static>),
|
||||
Part(MessagePart),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub struct Transform {
|
||||
pub variable: Box<VariableType>,
|
||||
pub functions: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub struct HeaderVariable<'x> {
|
||||
pub name: Vec<HeaderName<'x>>,
|
||||
pub part: HeaderPart,
|
||||
pub index_hdr: i32,
|
||||
pub index_part: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum MessagePart {
|
||||
TextBody(bool),
|
||||
HtmlBody(bool),
|
||||
Contents,
|
||||
Raw,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum HeaderPart {
|
||||
Text,
|
||||
Date,
|
||||
Id,
|
||||
Address(AddressPart),
|
||||
ContentType(ContentTypePart),
|
||||
Received(ReceivedPart),
|
||||
Raw,
|
||||
RawName,
|
||||
Exists,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum ContentTypePart {
|
||||
Type,
|
||||
Subtype,
|
||||
Attribute(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum ReceivedPart {
|
||||
From(ReceivedHostname),
|
||||
FromIp,
|
||||
FromIpRev,
|
||||
By(ReceivedHostname),
|
||||
For,
|
||||
With,
|
||||
TlsVersion,
|
||||
TlsCipher,
|
||||
Id,
|
||||
Ident,
|
||||
Via,
|
||||
Date,
|
||||
DateRaw,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum ReceivedHostname {
|
||||
Name,
|
||||
Ip,
|
||||
Any,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[cfg_attr(
|
||||
any(test, feature = "serde"),
|
||||
derive(serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "rkyv",
|
||||
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
|
||||
)]
|
||||
pub enum Number {
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
impl Number {
|
||||
#[cfg(test)]
|
||||
pub fn to_float(&self) -> f64 {
|
||||
match self {
|
||||
Number::Integer(i) => *i as f64,
|
||||
Number::Float(fl) => *fl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for usize {
|
||||
fn from(value: Number) -> Self {
|
||||
match value {
|
||||
Number::Integer(i) => i as usize,
|
||||
Number::Float(fl) => fl as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Number {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Number::Integer(i) => i.fmt(f),
|
||||
Number::Float(fl) => fl.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
pub const VERSION: u32 = 2;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Compiler {
|
||||
max_script_size: 1024 * 1024,
|
||||
max_string_size: 4096,
|
||||
max_variable_name_size: 32,
|
||||
max_nested_blocks: 15,
|
||||
max_nested_tests: 15,
|
||||
max_nested_foreverypart: 3,
|
||||
max_match_variables: 30,
|
||||
max_local_variables: 128,
|
||||
max_header_size: 1024,
|
||||
max_includes: 6,
|
||||
functions: AHashMap::new(),
|
||||
no_capability_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_max_header_size(&mut self, size: usize) {
|
||||
self.max_header_size = size;
|
||||
}
|
||||
|
||||
pub fn with_max_header_size(mut self, size: usize) -> Self {
|
||||
self.max_header_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_includes(&mut self, size: usize) {
|
||||
self.max_includes = size;
|
||||
}
|
||||
|
||||
pub fn with_max_includes(mut self, size: usize) -> Self {
|
||||
self.max_includes = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_nested_blocks(&mut self, size: usize) {
|
||||
self.max_nested_blocks = size;
|
||||
}
|
||||
|
||||
pub fn with_max_nested_blocks(mut self, size: usize) -> Self {
|
||||
self.max_nested_blocks = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_nested_tests(&mut self, size: usize) {
|
||||
self.max_nested_tests = size;
|
||||
}
|
||||
|
||||
pub fn with_max_nested_tests(mut self, size: usize) -> Self {
|
||||
self.max_nested_tests = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_nested_foreverypart(&mut self, size: usize) {
|
||||
self.max_nested_foreverypart = size;
|
||||
}
|
||||
|
||||
pub fn with_max_nested_foreverypart(mut self, size: usize) -> Self {
|
||||
self.max_nested_foreverypart = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_script_size(&mut self, size: usize) {
|
||||
self.max_script_size = size;
|
||||
}
|
||||
|
||||
pub fn with_max_script_size(mut self, size: usize) -> Self {
|
||||
self.max_script_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_string_size(&mut self, size: usize) {
|
||||
self.max_string_size = size;
|
||||
}
|
||||
|
||||
pub fn with_max_string_size(mut self, size: usize) -> Self {
|
||||
self.max_string_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_variable_name_size(&mut self, size: usize) {
|
||||
self.max_variable_name_size = size;
|
||||
}
|
||||
|
||||
pub fn with_max_variable_name_size(mut self, size: usize) -> Self {
|
||||
self.max_variable_name_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_match_variables(&mut self, size: usize) {
|
||||
self.max_match_variables = size;
|
||||
}
|
||||
|
||||
pub fn with_max_match_variables(mut self, size: usize) -> Self {
|
||||
self.max_match_variables = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_local_variables(&mut self, size: usize) {
|
||||
self.max_local_variables = size;
|
||||
}
|
||||
|
||||
pub fn with_max_local_variables(mut self, size: usize) -> Self {
|
||||
self.max_local_variables = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register_functions(mut self, fnc_map: &mut FunctionMap) -> Self {
|
||||
self.functions = std::mem::take(&mut fnc_map.map);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_no_capability_check(mut self, value: bool) -> Self {
|
||||
self.no_capability_check = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_no_capability_check(&mut self, value: bool) {
|
||||
self.no_capability_check = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl CompileError {
|
||||
pub fn line_num(&self) -> usize {
|
||||
self.line_num
|
||||
}
|
||||
|
||||
pub fn line_pos(&self) -> usize {
|
||||
self.line_pos
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> &ErrorType {
|
||||
&self.error_type
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Regex {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.expr == other.expr
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Regex {}
|
||||
|
||||
impl TokenInfo {
|
||||
pub fn expected(self, expected: impl Into<Cow<'static, str>>) -> CompileError {
|
||||
CompileError {
|
||||
line_num: self.line_num,
|
||||
line_pos: self.line_pos,
|
||||
error_type: ErrorType::UnexpectedToken {
|
||||
expected: expected.into(),
|
||||
found: self.token.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn missing_tag(self, tag: impl Into<Cow<'static, str>>) -> CompileError {
|
||||
CompileError {
|
||||
line_num: self.line_num,
|
||||
line_pos: self.line_pos,
|
||||
error_type: ErrorType::MissingTag(tag.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn custom(self, error_type: ErrorType) -> CompileError {
|
||||
CompileError {
|
||||
line_num: self.line_num,
|
||||
line_pos: self.line_pos,
|
||||
error_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LazyRegex {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(ArcSwap::new(Arc::new(None))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Regex {
|
||||
pub fn new(expr: String, regex: fancy_regex::Regex) -> Self {
|
||||
Self {
|
||||
expr,
|
||||
regex: LazyRegex(Arc::new(ArcSwap::new(Arc::new(Some(regex))))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CompileError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.error_type() {
|
||||
ErrorType::InvalidCharacter(value) => {
|
||||
write!(f, "Invalid character {:?}", char::from(*value))
|
||||
}
|
||||
ErrorType::InvalidNumber(value) => write!(f, "Invalid number {value:?}"),
|
||||
ErrorType::InvalidMatchVariable(value) => {
|
||||
write!(f, "Match variable {value} out of range")
|
||||
}
|
||||
ErrorType::InvalidUnicodeSequence(value) => {
|
||||
write!(f, "Invalid Unicode sequence {value:04x}")
|
||||
}
|
||||
ErrorType::InvalidNamespace(value) => write!(f, "Invalid namespace {value:?}"),
|
||||
ErrorType::InvalidRegex(value) => write!(f, "Invalid regular expression {value:?}"),
|
||||
ErrorType::InvalidExpression(value) => write!(f, "Invalid expression {value}"),
|
||||
ErrorType::InvalidUtf8String => write!(f, "Invalid UTF-8 string"),
|
||||
ErrorType::InvalidHeaderName => write!(f, "Invalid header name"),
|
||||
ErrorType::InvalidArguments => write!(f, "Invalid Arguments"),
|
||||
ErrorType::InvalidAddress => write!(f, "Invalid Address"),
|
||||
ErrorType::InvalidURI => write!(f, "Invalid URI"),
|
||||
ErrorType::InvalidEnvelope(value) => write!(f, "Invalid envelope {value:?}"),
|
||||
ErrorType::UnterminatedString => write!(f, "Unterminated string"),
|
||||
ErrorType::UnterminatedComment => write!(f, "Unterminated comment"),
|
||||
ErrorType::UnterminatedMultiline => write!(f, "Unterminated multi-line string"),
|
||||
ErrorType::UnterminatedBlock => write!(f, "Unterminated block"),
|
||||
ErrorType::ScriptTooLong => write!(f, "Sieve script is too large"),
|
||||
ErrorType::StringTooLong => write!(f, "String is too long"),
|
||||
ErrorType::VariableTooLong => write!(f, "Variable name is too long"),
|
||||
ErrorType::VariableIsLocal(value) => {
|
||||
write!(f, "Variable {value:?} was already defined as local")
|
||||
}
|
||||
ErrorType::HeaderTooLong => write!(f, "Header value is too long"),
|
||||
ErrorType::ExpectedConstantString => write!(f, "Expected a constant string"),
|
||||
ErrorType::UnexpectedToken { expected, found } => {
|
||||
write!(f, "Expected token {expected:?} but found {found:?}")
|
||||
}
|
||||
ErrorType::UnexpectedEOF => write!(f, "Unexpected end of file"),
|
||||
ErrorType::TooManyNestedBlocks => write!(f, "Too many nested blocks"),
|
||||
ErrorType::TooManyNestedTests => write!(f, "Too many nested tests"),
|
||||
ErrorType::TooManyNestedForEveryParts => {
|
||||
write!(f, "Too many nested foreverypart blocks")
|
||||
}
|
||||
ErrorType::TooManyIncludes => write!(f, "Too many includes"),
|
||||
ErrorType::LabelAlreadyDefined(value) => write!(f, "Label {value:?} already defined"),
|
||||
ErrorType::LabelUndefined(value) => write!(f, "Label {value:?} does not exist"),
|
||||
ErrorType::BreakOutsideLoop => write!(f, "Break used outside of foreverypart loop"),
|
||||
ErrorType::ContinueOutsideLoop => write!(f, "Continue used outside of while loop"),
|
||||
ErrorType::UnsupportedComparator(value) => {
|
||||
write!(f, "Comparator {value:?} is not supported")
|
||||
}
|
||||
ErrorType::DuplicatedParameter => write!(f, "Duplicated argument"),
|
||||
ErrorType::UndeclaredCapability(value) => {
|
||||
write!(f, "Undeclared capability '{value}'")
|
||||
}
|
||||
ErrorType::MissingTag(value) => write!(f, "Missing tag {value:?}"),
|
||||
}?;
|
||||
|
||||
write!(
|
||||
f,
|
||||
" at line {}, column {}.",
|
||||
self.line_num(),
|
||||
self.line_pos()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RuntimeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RuntimeError::TooManyIncludes => write!(f, ""),
|
||||
RuntimeError::InvalidInstruction(value) => write!(
|
||||
f,
|
||||
"Script executed invalid instruction {:?} at line {}, column {}.",
|
||||
value.name(),
|
||||
value.line_pos(),
|
||||
value.line_num()
|
||||
),
|
||||
RuntimeError::ScriptErrorMessage(value) => {
|
||||
write!(f, "Script reported error {value:?}.")
|
||||
}
|
||||
RuntimeError::CapabilityNotAllowed(value) => {
|
||||
write!(f, "Capability '{value}' has been disabled.")
|
||||
}
|
||||
RuntimeError::CapabilityNotSupported(value) => {
|
||||
write!(f, "Capability '{value}' not supported.")
|
||||
}
|
||||
RuntimeError::CPULimitReached => write!(
|
||||
f,
|
||||
"Script exceeded the maximum number of instructions allowed to execute."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::Compiler;
|
||||
|
||||
#[test]
|
||||
fn parse_rfc() {
|
||||
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_dir.push("tests");
|
||||
test_dir.push("rfcs");
|
||||
let mut tests_run = 0;
|
||||
|
||||
let compiler = Compiler::new().with_max_nested_foreverypart(10);
|
||||
|
||||
for file_name in fs::read_dir(&test_dir).unwrap() {
|
||||
let mut file_name = file_name.unwrap().path();
|
||||
if file_name.extension().is_some_and(|e| e == "sieve") {
|
||||
println!("Parsing {}", file_name.display());
|
||||
|
||||
/*if !file_name
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("plugins")
|
||||
{
|
||||
let test = "true";
|
||||
continue;
|
||||
}*/
|
||||
|
||||
let script = fs::read(&file_name).unwrap();
|
||||
file_name.set_extension("json");
|
||||
let expected_result = fs::read(&file_name).unwrap();
|
||||
|
||||
tests_run += 1;
|
||||
|
||||
let sieve = compiler.compile(&script).unwrap();
|
||||
let json_sieve = serde_json::to_string_pretty(
|
||||
&sieve
|
||||
.instructions
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if json_sieve.as_bytes() != expected_result {
|
||||
file_name.set_extension("failed");
|
||||
fs::write(&file_name, json_sieve.as_bytes()).unwrap();
|
||||
panic!("Test failed, parsed sieve saved to {}", file_name.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
tests_run > 0,
|
||||
"Did not find any tests to run in folder {}.",
|
||||
test_dir.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user