first commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
package sieve
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ── AST ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
type Script struct {
|
||||
Statements []Statement
|
||||
}
|
||||
|
||||
// Statement is either an Action or an IfStatement.
|
||||
type Statement interface{ isStatement() }
|
||||
|
||||
type Action struct {
|
||||
Name string // "fileinto" | "discard" | "keep" | "stop"
|
||||
Arg string // folder name for fileinto, empty otherwise
|
||||
}
|
||||
|
||||
func (Action) isStatement() {}
|
||||
|
||||
type IfStatement struct {
|
||||
Test Test
|
||||
Then []Statement
|
||||
ElseIfs []ElseIf
|
||||
Else []Statement
|
||||
HasElse bool
|
||||
}
|
||||
|
||||
func (IfStatement) isStatement() {}
|
||||
|
||||
type ElseIf struct {
|
||||
Test Test
|
||||
Then []Statement
|
||||
}
|
||||
|
||||
// Test is a condition — this pass supports only header tests, the
|
||||
// overwhelming majority of real-world filtering rules.
|
||||
type Test struct {
|
||||
Kind string // "header" | "true"
|
||||
MatchType string // "contains" | "is"
|
||||
Header string
|
||||
Value string
|
||||
}
|
||||
|
||||
// ── Parser ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type parser struct {
|
||||
lex *lexer
|
||||
cur token
|
||||
}
|
||||
|
||||
func Parse(script string) (*Script, error) {
|
||||
p := &parser{lex: newLexer(script)}
|
||||
if err := p.advance(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Script{}
|
||||
for p.cur.kind != tokEOF {
|
||||
stmt, err := p.parseStatement()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Statements = append(s.Statements, stmt)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (p *parser) advance() error {
|
||||
t, err := p.lex.next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.cur = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) expect(kind tokenKind, desc string) (token, error) {
|
||||
if p.cur.kind != kind {
|
||||
return token{}, fmt.Errorf("expected %s, got %q", desc, p.cur.value)
|
||||
}
|
||||
t := p.cur
|
||||
if err := p.advance(); err != nil {
|
||||
return token{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseStatement() (Statement, error) {
|
||||
if p.cur.kind != tokIdent {
|
||||
return nil, fmt.Errorf("expected statement, got %q", p.cur.value)
|
||||
}
|
||||
|
||||
switch p.cur.value {
|
||||
case "if":
|
||||
return p.parseIf()
|
||||
case "fileinto":
|
||||
if err := p.advance(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arg, err := p.expect(tokString, "folder name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := p.expect(tokSemicolon, ";"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Action{Name: "fileinto", Arg: arg.value}, nil
|
||||
case "discard", "keep", "stop":
|
||||
name := p.cur.value
|
||||
if err := p.advance(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := p.expect(tokSemicolon, ";"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Action{Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported command %q", p.cur.value)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parseIf() (Statement, error) {
|
||||
if err := p.advance(); err != nil { // skip "if"
|
||||
return nil, err
|
||||
}
|
||||
test, err := p.parseTest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
then, err := p.parseBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmt := IfStatement{Test: test, Then: then}
|
||||
|
||||
for p.cur.kind == tokIdent && p.cur.value == "elsif" {
|
||||
if err := p.advance(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elifTest, err := p.parseTest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elifThen, err := p.parseBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmt.ElseIfs = append(stmt.ElseIfs, ElseIf{Test: elifTest, Then: elifThen})
|
||||
}
|
||||
|
||||
if p.cur.kind == tokIdent && p.cur.value == "else" {
|
||||
if err := p.advance(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elseBlock, err := p.parseBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmt.Else = elseBlock
|
||||
stmt.HasElse = true
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseTest() (Test, error) {
|
||||
if p.cur.kind != tokIdent {
|
||||
return Test{}, fmt.Errorf("expected test, got %q", p.cur.value)
|
||||
}
|
||||
|
||||
if p.cur.value == "true" {
|
||||
if err := p.advance(); err != nil {
|
||||
return Test{}, err
|
||||
}
|
||||
return Test{Kind: "true"}, nil
|
||||
}
|
||||
|
||||
if p.cur.value != "header" {
|
||||
return Test{}, fmt.Errorf("unsupported test %q (only 'header' and 'true' supported)", p.cur.value)
|
||||
}
|
||||
if err := p.advance(); err != nil {
|
||||
return Test{}, err
|
||||
}
|
||||
|
||||
if p.cur.kind != tokTag {
|
||||
return Test{}, fmt.Errorf("expected match type (:contains or :is), got %q", p.cur.value)
|
||||
}
|
||||
matchType := p.cur.value[1:] // strip leading ':'
|
||||
if matchType != "contains" && matchType != "is" {
|
||||
return Test{}, fmt.Errorf("unsupported match type %q (only :contains and :is supported)", matchType)
|
||||
}
|
||||
if err := p.advance(); err != nil {
|
||||
return Test{}, err
|
||||
}
|
||||
|
||||
headerTok, err := p.expect(tokString, "header name")
|
||||
if err != nil {
|
||||
return Test{}, err
|
||||
}
|
||||
valueTok, err := p.expect(tokString, "match value")
|
||||
if err != nil {
|
||||
return Test{}, err
|
||||
}
|
||||
|
||||
return Test{Kind: "header", MatchType: matchType, Header: headerTok.value, Value: valueTok.value}, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseBlock() ([]Statement, error) {
|
||||
if _, err := p.expect(tokLBrace, "{"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stmts []Statement
|
||||
for p.cur.kind != tokRBrace {
|
||||
if p.cur.kind == tokEOF {
|
||||
return nil, fmt.Errorf("unterminated block, expected }")
|
||||
}
|
||||
stmt, err := p.parseStatement()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmts = append(stmts, stmt)
|
||||
}
|
||||
if _, err := p.expect(tokRBrace, "}"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stmts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user