Files
2026-08-09 18:03:09 +01:00

230 lines
5.3 KiB
Go

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
}