Skip to main content

sieve/compiler/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use self::{
8    grammar::{AddressPart, Capability},
9    lexer::{StringConstant, tokenizer::TokenInfo},
10};
11use crate::{
12    Compiler, Envelope, FunctionMap,
13    runtime::{RuntimeError, tests::glob::CompiledGlob},
14};
15use ahash::AHashMap;
16use mail_parser::HeaderName;
17use std::{borrow::Cow, fmt::Display};
18
19pub(crate) mod emit;
20pub mod grammar;
21pub mod lexer;
22
23#[derive(Debug)]
24pub struct CompileError {
25    line_num: usize,
26    line_pos: usize,
27    error_type: ErrorType,
28}
29
30#[derive(Debug)]
31pub enum ErrorType {
32    InvalidCharacter(u8),
33    InvalidNumber(String),
34    InvalidMatchVariable(usize),
35    InvalidUnicodeSequence(u32),
36    InvalidNamespace(String),
37    InvalidRegex(String),
38    InvalidExpression(String),
39    InvalidUtf8String,
40    InvalidHeaderName,
41    InvalidArguments,
42    InvalidAddress,
43    InvalidURI,
44    InvalidEnvelope(String),
45    UnterminatedString,
46    UnterminatedComment,
47    UnterminatedMultiline,
48    UnterminatedBlock,
49    ScriptTooLong,
50    StringTooLong,
51    VariableTooLong,
52    VariableIsLocal(String),
53    HeaderTooLong,
54    ExpectedConstantString,
55    UnexpectedToken {
56        expected: Cow<'static, str>,
57        found: String,
58    },
59    UnexpectedEOF,
60    TooManyNestedBlocks,
61    TooManyNestedTests,
62    TooManyNestedForEveryParts,
63    TooManyIncludes,
64    LabelAlreadyDefined(String),
65    LabelUndefined(String),
66    BreakOutsideLoop,
67    ContinueOutsideLoop,
68    UnsupportedComparator(String),
69    DuplicatedParameter,
70    UndeclaredCapability(Capability),
71    MissingTag(Cow<'static, str>),
72}
73
74impl Default for Compiler {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[cfg_attr(
82    any(test, feature = "serde"),
83    derive(serde::Serialize, serde::Deserialize)
84)]
85#[repr(u8)]
86pub(crate) enum Value {
87    Text(ConstantId) = 0,
88    Number(Number) = 1,
89    Variable(VariableType) = 2,
90    Regex(Regex) = 3,
91    Glob(Glob) = 4,
92    Header(HeaderName<'static>) = 5,
93    List(Box<[Value]>) = 6,
94}
95
96#[derive(Debug)]
97pub(crate) enum RawValue {
98    Text(String),
99    Number(Number),
100    Value(Value),
101}
102
103impl From<StringConstant> for RawValue {
104    fn from(value: StringConstant) -> Self {
105        match value {
106            StringConstant::String(text) => RawValue::Text(text),
107            StringConstant::Number(number) => RawValue::Number(number),
108        }
109    }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113#[cfg_attr(
114    any(test, feature = "serde"),
115    derive(serde::Serialize, serde::Deserialize)
116)]
117#[repr(transparent)]
118pub struct ConstantId(u32);
119
120impl ConstantId {
121    pub(crate) const UNRESOLVED: ConstantId = ConstantId(u32::MAX);
122
123    #[inline(always)]
124    pub(crate) fn new(index: usize) -> Self {
125        ConstantId(index as u32)
126    }
127
128    #[inline(always)]
129    pub(crate) fn index(&self) -> usize {
130        self.0 as usize
131    }
132}
133
134#[derive(Debug, Clone)]
135#[cfg_attr(
136    any(test, feature = "serde"),
137    derive(serde::Serialize, serde::Deserialize)
138)]
139pub struct Regex {
140    pub expr: String,
141}
142
143#[derive(Debug, Clone)]
144#[cfg_attr(
145    any(test, feature = "serde"),
146    derive(serde::Serialize, serde::Deserialize)
147)]
148pub struct Glob {
149    #[cfg_attr(any(test, feature = "serde"), serde(skip, default))]
150    pub(crate) glob: CompiledGlob,
151    pub expr: String,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(
156    any(test, feature = "serde"),
157    derive(serde::Serialize, serde::Deserialize)
158)]
159#[repr(u8)]
160pub enum VariableType {
161    Local(u16) = 0,
162    Match(u8) = 1,
163    Global(String) = 2,
164    Environment(String) = 3,
165    Envelope(Envelope) = 4,
166    Header(HeaderVariable<'static>) = 5,
167    Part(MessagePart) = 6,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
171#[cfg_attr(
172    any(test, feature = "serde"),
173    derive(serde::Serialize, serde::Deserialize)
174)]
175pub struct Transform {
176    pub variable: Box<VariableType>,
177    pub functions: Box<[u32]>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181#[cfg_attr(
182    any(test, feature = "serde"),
183    derive(serde::Serialize, serde::Deserialize)
184)]
185pub struct HeaderVariable<'x> {
186    pub name: Box<[HeaderName<'x>]>,
187    pub part: HeaderPart,
188    pub index_hdr: i32,
189    pub index_part: i32,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Copy)]
193#[cfg_attr(
194    any(test, feature = "serde"),
195    derive(serde::Serialize, serde::Deserialize)
196)]
197#[repr(u8)]
198pub enum MessagePart {
199    TextBody(bool) = 0,
200    HtmlBody(bool) = 1,
201    Contents = 2,
202    Raw = 3,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206#[cfg_attr(
207    any(test, feature = "serde"),
208    derive(serde::Serialize, serde::Deserialize)
209)]
210#[repr(u8)]
211pub enum HeaderPart {
212    Text = 0,
213    Date = 1,
214    Id = 2,
215    Address(AddressPart) = 3,
216    ContentType(ContentTypePart) = 4,
217    Received(ReceivedPart) = 5,
218    Raw = 6,
219    RawName = 7,
220    Exists = 8,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224#[cfg_attr(
225    any(test, feature = "serde"),
226    derive(serde::Serialize, serde::Deserialize)
227)]
228#[repr(u8)]
229pub enum ContentTypePart {
230    Type = 0,
231    Subtype = 1,
232    Attribute(String) = 2,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Copy)]
236#[cfg_attr(
237    any(test, feature = "serde"),
238    derive(serde::Serialize, serde::Deserialize)
239)]
240#[repr(u8)]
241pub enum ReceivedPart {
242    From(ReceivedHostname) = 0,
243    FromIp = 1,
244    FromIpRev = 2,
245    By(ReceivedHostname) = 3,
246    For = 4,
247    With = 5,
248    TlsVersion = 6,
249    TlsCipher = 7,
250    Id = 8,
251    Ident = 9,
252    Via = 10,
253    Date = 11,
254    DateRaw = 12,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Copy)]
258#[cfg_attr(
259    any(test, feature = "serde"),
260    derive(serde::Serialize, serde::Deserialize)
261)]
262#[repr(u8)]
263pub enum ReceivedHostname {
264    Name = 0,
265    Ip = 1,
266    Any = 2,
267}
268
269#[derive(Debug, Clone, Copy)]
270#[cfg_attr(
271    any(test, feature = "serde"),
272    derive(serde::Serialize, serde::Deserialize)
273)]
274#[repr(u8)]
275pub enum Number {
276    Integer(i64) = 0,
277    Float(f64) = 1,
278}
279
280impl ReceivedPart {
281    pub(crate) fn code(&self) -> u16 {
282        let (part, hostname): (u16, u16) = match self {
283            ReceivedPart::From(host) => (0, *host as u16),
284            ReceivedPart::FromIp => (1, 0),
285            ReceivedPart::FromIpRev => (2, 0),
286            ReceivedPart::By(host) => (3, *host as u16),
287            ReceivedPart::For => (4, 0),
288            ReceivedPart::With => (5, 0),
289            ReceivedPart::TlsVersion => (6, 0),
290            ReceivedPart::TlsCipher => (7, 0),
291            ReceivedPart::Id => (8, 0),
292            ReceivedPart::Ident => (9, 0),
293            ReceivedPart::Via => (10, 0),
294            ReceivedPart::Date => (11, 0),
295            ReceivedPart::DateRaw => (12, 0),
296        };
297        part | (hostname << 8)
298    }
299
300    pub(crate) fn from_code(part: u8, hostname: u8) -> ReceivedPart {
301        let hostname = match hostname {
302            0 => ReceivedHostname::Name,
303            1 => ReceivedHostname::Ip,
304            _ => ReceivedHostname::Any,
305        };
306        match part {
307            0 => ReceivedPart::From(hostname),
308            1 => ReceivedPart::FromIp,
309            2 => ReceivedPart::FromIpRev,
310            3 => ReceivedPart::By(hostname),
311            4 => ReceivedPart::For,
312            5 => ReceivedPart::With,
313            6 => ReceivedPart::TlsVersion,
314            7 => ReceivedPart::TlsCipher,
315            8 => ReceivedPart::Id,
316            9 => ReceivedPart::Ident,
317            10 => ReceivedPart::Via,
318            11 => ReceivedPart::Date,
319            _ => ReceivedPart::DateRaw,
320        }
321    }
322}
323
324impl Number {
325    #[cfg(test)]
326    pub fn to_float(&self) -> f64 {
327        match self {
328            Number::Integer(i) => *i as f64,
329            Number::Float(fl) => *fl,
330        }
331    }
332}
333
334impl From<Number> for usize {
335    fn from(value: Number) -> Self {
336        match value {
337            Number::Integer(i) => i as usize,
338            Number::Float(fl) => fl as usize,
339        }
340    }
341}
342
343impl Display for Number {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        match self {
346            Number::Integer(i) => i.fmt(f),
347            Number::Float(fl) => fl.fmt(f),
348        }
349    }
350}
351
352impl Compiler {
353    pub const VERSION: u32 = crate::bytecode::FORMAT_VERSION as u32;
354
355    pub fn new() -> Self {
356        Compiler {
357            max_script_size: 1024 * 1024,
358            max_string_size: 4096,
359            max_variable_name_size: 32,
360            max_nested_blocks: 15,
361            max_nested_tests: 15,
362            max_nested_foreverypart: 3,
363            max_match_variables: 30,
364            max_local_variables: 128,
365            max_header_size: 1024,
366            max_includes: 6,
367            functions: AHashMap::new(),
368            no_capability_check: false,
369        }
370    }
371
372    pub fn set_max_header_size(&mut self, size: usize) {
373        self.max_header_size = size;
374    }
375
376    pub fn with_max_header_size(mut self, size: usize) -> Self {
377        self.max_header_size = size;
378        self
379    }
380
381    pub fn set_max_includes(&mut self, size: usize) {
382        self.max_includes = size;
383    }
384
385    pub fn with_max_includes(mut self, size: usize) -> Self {
386        self.max_includes = size;
387        self
388    }
389
390    pub fn set_max_nested_blocks(&mut self, size: usize) {
391        self.max_nested_blocks = size;
392    }
393
394    pub fn with_max_nested_blocks(mut self, size: usize) -> Self {
395        self.max_nested_blocks = size;
396        self
397    }
398
399    pub fn set_max_nested_tests(&mut self, size: usize) {
400        self.max_nested_tests = size;
401    }
402
403    pub fn with_max_nested_tests(mut self, size: usize) -> Self {
404        self.max_nested_tests = size;
405        self
406    }
407
408    pub fn set_max_nested_foreverypart(&mut self, size: usize) {
409        self.max_nested_foreverypart = size;
410    }
411
412    pub fn with_max_nested_foreverypart(mut self, size: usize) -> Self {
413        self.max_nested_foreverypart = size;
414        self
415    }
416
417    pub fn set_max_script_size(&mut self, size: usize) {
418        self.max_script_size = size;
419    }
420
421    pub fn with_max_script_size(mut self, size: usize) -> Self {
422        self.max_script_size = size;
423        self
424    }
425
426    pub fn set_max_string_size(&mut self, size: usize) {
427        self.max_string_size = size;
428    }
429
430    pub fn with_max_string_size(mut self, size: usize) -> Self {
431        self.max_string_size = size;
432        self
433    }
434
435    pub fn set_max_variable_name_size(&mut self, size: usize) {
436        self.max_variable_name_size = size;
437    }
438
439    pub fn with_max_variable_name_size(mut self, size: usize) -> Self {
440        self.max_variable_name_size = size;
441        self
442    }
443
444    pub fn set_max_match_variables(&mut self, size: usize) {
445        self.max_match_variables = size;
446    }
447
448    pub fn with_max_match_variables(mut self, size: usize) -> Self {
449        self.max_match_variables = size;
450        self
451    }
452
453    pub fn set_max_local_variables(&mut self, size: usize) {
454        self.max_local_variables = size;
455    }
456
457    pub fn with_max_local_variables(mut self, size: usize) -> Self {
458        self.max_local_variables = size;
459        self
460    }
461
462    pub fn register_functions(mut self, fnc_map: &mut FunctionMap) -> Self {
463        self.functions = std::mem::take(&mut fnc_map.map);
464        self
465    }
466
467    pub fn with_no_capability_check(mut self, value: bool) -> Self {
468        self.no_capability_check = value;
469        self
470    }
471
472    pub fn set_no_capability_check(&mut self, value: bool) {
473        self.no_capability_check = value;
474    }
475}
476
477impl CompileError {
478    pub fn line_num(&self) -> usize {
479        self.line_num
480    }
481
482    pub fn line_pos(&self) -> usize {
483        self.line_pos
484    }
485
486    pub fn error_type(&self) -> &ErrorType {
487        &self.error_type
488    }
489}
490
491impl PartialEq for Regex {
492    fn eq(&self, other: &Self) -> bool {
493        self.expr == other.expr
494    }
495}
496
497impl Eq for Regex {}
498
499impl TokenInfo {
500    pub fn expected(self, expected: impl Into<Cow<'static, str>>) -> CompileError {
501        CompileError {
502            line_num: self.line_num,
503            line_pos: self.line_pos,
504            error_type: ErrorType::UnexpectedToken {
505                expected: expected.into(),
506                found: self.token.to_string(),
507            },
508        }
509    }
510
511    pub fn missing_tag(self, tag: impl Into<Cow<'static, str>>) -> CompileError {
512        CompileError {
513            line_num: self.line_num,
514            line_pos: self.line_pos,
515            error_type: ErrorType::MissingTag(tag.into()),
516        }
517    }
518
519    pub fn custom(self, error_type: ErrorType) -> CompileError {
520        CompileError {
521            line_num: self.line_num,
522            line_pos: self.line_pos,
523            error_type,
524        }
525    }
526}
527
528impl Glob {
529    pub fn new(expr: String, to_lower: bool) -> Self {
530        let glob = CompiledGlob::compile(&expr, to_lower);
531        Self { expr, glob }
532    }
533}
534
535impl PartialEq for Glob {
536    fn eq(&self, other: &Self) -> bool {
537        self.expr == other.expr
538    }
539}
540
541impl Eq for Glob {}
542
543impl Regex {
544    pub fn new(expr: String) -> Self {
545        Self { expr }
546    }
547}
548
549impl Display for CompileError {
550    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551        match &self.error_type() {
552            ErrorType::InvalidCharacter(value) => {
553                write!(f, "Invalid character {:?}", char::from(*value))
554            }
555            ErrorType::InvalidNumber(value) => write!(f, "Invalid number {value:?}"),
556            ErrorType::InvalidMatchVariable(value) => {
557                write!(f, "Match variable {value} out of range")
558            }
559            ErrorType::InvalidUnicodeSequence(value) => {
560                write!(f, "Invalid Unicode sequence {value:04x}")
561            }
562            ErrorType::InvalidNamespace(value) => write!(f, "Invalid namespace {value:?}"),
563            ErrorType::InvalidRegex(value) => write!(f, "Invalid regular expression {value:?}"),
564            ErrorType::InvalidExpression(value) => write!(f, "Invalid expression {value}"),
565            ErrorType::InvalidUtf8String => write!(f, "Invalid UTF-8 string"),
566            ErrorType::InvalidHeaderName => write!(f, "Invalid header name"),
567            ErrorType::InvalidArguments => write!(f, "Invalid Arguments"),
568            ErrorType::InvalidAddress => write!(f, "Invalid Address"),
569            ErrorType::InvalidURI => write!(f, "Invalid URI"),
570            ErrorType::InvalidEnvelope(value) => write!(f, "Invalid envelope {value:?}"),
571            ErrorType::UnterminatedString => write!(f, "Unterminated string"),
572            ErrorType::UnterminatedComment => write!(f, "Unterminated comment"),
573            ErrorType::UnterminatedMultiline => write!(f, "Unterminated multi-line string"),
574            ErrorType::UnterminatedBlock => write!(f, "Unterminated block"),
575            ErrorType::ScriptTooLong => write!(f, "Sieve script is too large"),
576            ErrorType::StringTooLong => write!(f, "String is too long"),
577            ErrorType::VariableTooLong => write!(f, "Variable name is too long"),
578            ErrorType::VariableIsLocal(value) => {
579                write!(f, "Variable {value:?} was already defined as local")
580            }
581            ErrorType::HeaderTooLong => write!(f, "Header value is too long"),
582            ErrorType::ExpectedConstantString => write!(f, "Expected a constant string"),
583            ErrorType::UnexpectedToken { expected, found } => {
584                write!(f, "Expected token {expected:?} but found {found:?}")
585            }
586            ErrorType::UnexpectedEOF => write!(f, "Unexpected end of file"),
587            ErrorType::TooManyNestedBlocks => write!(f, "Too many nested blocks"),
588            ErrorType::TooManyNestedTests => write!(f, "Too many nested tests"),
589            ErrorType::TooManyNestedForEveryParts => {
590                write!(f, "Too many nested foreverypart blocks")
591            }
592            ErrorType::TooManyIncludes => write!(f, "Too many includes"),
593            ErrorType::LabelAlreadyDefined(value) => write!(f, "Label {value:?} already defined"),
594            ErrorType::LabelUndefined(value) => write!(f, "Label {value:?} does not exist"),
595            ErrorType::BreakOutsideLoop => write!(f, "Break used outside of foreverypart loop"),
596            ErrorType::ContinueOutsideLoop => write!(f, "Continue used outside of while loop"),
597            ErrorType::UnsupportedComparator(value) => {
598                write!(f, "Comparator {value:?} is not supported")
599            }
600            ErrorType::DuplicatedParameter => write!(f, "Duplicated argument"),
601            ErrorType::UndeclaredCapability(value) => {
602                write!(f, "Undeclared capability '{value}'")
603            }
604            ErrorType::MissingTag(value) => write!(f, "Missing tag {value:?}"),
605        }?;
606
607        write!(
608            f,
609            " at line {}, column {}.",
610            self.line_num(),
611            self.line_pos()
612        )
613    }
614}
615
616impl Display for RuntimeError {
617    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618        match self {
619            RuntimeError::TooManyIncludes => f.write_str("Too many nested includes"),
620            RuntimeError::ScriptNotFound(name) => write!(f, "Included script {name:?} not found."),
621            RuntimeError::InvalidInstruction {
622                name,
623                line_num,
624                line_pos,
625            } => write!(
626                f,
627                "Script executed invalid instruction {name:?} at line {line_num}, column {line_pos}."
628            ),
629            RuntimeError::ScriptErrorMessage(value) => {
630                write!(f, "Script reported error {value:?}.")
631            }
632            RuntimeError::CapabilityNotAllowed(value) => {
633                write!(f, "Capability '{value}' has been disabled.")
634            }
635            RuntimeError::CapabilityNotSupported(value) => {
636                write!(f, "Capability '{value}' not supported.")
637            }
638            RuntimeError::CPULimitReached => write!(
639                f,
640                "Script exceeded the maximum number of instructions allowed to execute."
641            ),
642            RuntimeError::MemoryLimitReached => {
643                write!(f, "Script exceeded the maximum amount of memory allowed.")
644            }
645            RuntimeError::InvalidBytecode => write!(f, "Compiled script is corrupted."),
646            RuntimeError::AwaitingInput => f.write_str("Script is waiting for a pending result"),
647        }
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use std::{fs, path::PathBuf};
654
655    use crate::Compiler;
656
657    #[test]
658    fn parse_rfc() {
659        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
660        test_dir.push("tests");
661        test_dir.push("rfcs");
662        let mut tests_run = 0;
663
664        let compiler = Compiler::new().with_max_nested_foreverypart(10);
665
666        for file_name in fs::read_dir(&test_dir).unwrap() {
667            let mut file_name = file_name.unwrap().path();
668            if file_name.extension().is_some_and(|e| e == "sieve") {
669                println!("Parsing {}", file_name.display());
670
671                /*if !file_name
672                    .file_name()
673                    .unwrap()
674                    .to_str()
675                    .unwrap()
676                    .contains("plugins")
677                {
678                    let test = "true";
679                    continue;
680                }*/
681
682                let script = fs::read(&file_name).unwrap();
683                file_name.set_extension("json");
684                let expected_result = fs::read(&file_name).unwrap();
685
686                tests_run += 1;
687
688                let program = compiler.compile_ast(&script).unwrap();
689                let sieve = program.emit().unwrap();
690                assert_eq!(
691                    crate::Sieve::from_bytes(&sieve.to_bytes()).unwrap(),
692                    sieve,
693                    "bytecode round trip altered {}",
694                    file_name.display()
695                );
696
697                let json_sieve = serde_json::to_string_pretty(
698                    &program
699                        .instructions
700                        .into_iter()
701                        .enumerate()
702                        .collect::<Vec<_>>(),
703                )
704                .unwrap();
705
706                if json_sieve.as_bytes() != expected_result {
707                    file_name.set_extension("failed");
708                    fs::write(&file_name, json_sieve.as_bytes()).unwrap();
709                    panic!("Test failed, parsed sieve saved to {}", file_name.display());
710                }
711            }
712        }
713
714        assert!(
715            tests_run > 0,
716            "Did not find any tests to run in folder {}.",
717            test_dir.display()
718        );
719    }
720}