Skip to content

Commit

Permalink
Implement relational extension
Browse files Browse the repository at this point in the history
  • Loading branch information
foxcpp committed Jan 30, 2024
1 parent 2564207 commit 72d6b00
Show file tree
Hide file tree
Showing 17 changed files with 540 additions and 64 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ implementation in Go.
- encoded-character ([RFC 5228])
- imap4flags ([RFC 5232])
- variables ([RFC 5229])
- relational ([RFC 5231])

## Example

See ./cmd/sieve-run.

## Known issues

- `:matches` `*` is greedy (RFC 5229 requires non-greedy matching).
- Some invalid scripts are accepted as valid (see tests/compile_test.go)
- Comments in addresses are not ignored when testing equality, etc.
- Source routes in addresses are not ignored when testing equality, etc.

[RFC 5228]: https://datatracker.ietf.org/doc/html/rfc5228
[RFC 5229]: https://datatracker.ietf.org/doc/html/rfc5229
[RFC 5232]: https://datatracker.ietf.org/doc/html/rfc5232
[RFC 5231]: https://datatracker.ietf.org/doc/html/rfc5231

2 changes: 2 additions & 0 deletions interp/dovecot_testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func (t TestDovecotCompile) Check(_ context.Context, d *RuntimeData) (bool, erro
}

toks, err := lexer.Lex(bytes.NewReader(svScript), &lexer.Options{
Filename: t.ScriptPath,
MaxTokens: 5000,
})
if err != nil {
Expand Down Expand Up @@ -197,6 +198,7 @@ func (t TestDovecotRun) Check(ctx context.Context, d *RuntimeData) (bool, error)
}

type TestDovecotTestError struct {
matcherTest
}

func (t TestDovecotTestError) Check(_ context.Context, _ *RuntimeData) (bool, error) {
Expand Down
1 change: 1 addition & 0 deletions interp/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var supportedRequires = map[string]struct{}{

"imap4flags": {},
"variables": {},
"relational": {},
}

var (
Expand Down
20 changes: 18 additions & 2 deletions interp/load_dovecot.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,23 @@ func loadDovecotRun(s *Script, test parser.Test) (Test, error) {
}

func loadDovecotError(s *Script, test parser.Test) (Test, error) {
loaded := TestDovecotTestError{}
err := LoadSpec(s, &Spec{}, test.Position, test.Args, test.Tests, nil)
loaded := TestDovecotTestError{matcherTest: newMatcherTest()}
err := LoadSpec(s, loaded.addSpecTags(&Spec{
Tags: map[string]SpecTag{
"index": {
NeedsValue: true,
MinStrCount: 1,
MaxStrCount: 1,
NoVariables: true,
MatchNum: func(val int) {},
},
},
Pos: []SpecPosArg{
{
MatchStr: func(val []string) {},
MinStrCount: 1,
},
},
}), test.Position, test.Args, test.Tests, nil)
return loaded, err
}
2 changes: 1 addition & 1 deletion interp/load_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func LoadSpec(s *Script, spec *Spec, position lexer.Position, args []parser.Arg,
}
tag, ok := spec.Tags[strings.ToLower(a.Value)]
if !ok {
return lexer.ErrorAt(a, "LoadSpec: unknown tagged argument: %v", a)
return lexer.ErrorAt(a, "LoadSpec: unknown tagged argument: %v", a.Value)
}
if tag.NeedsValue {
lastTag = &tag
Expand Down
8 changes: 5 additions & 3 deletions interp/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ if envelope :is "from" "[email protected]" {
`, []Cmd{
CmdIf{
Test: EnvelopeTest{
Comparator: ComparatorASCIICaseMap,
Match: MatchIs,
matcherTest: matcherTest{
comparator: ComparatorASCIICaseMap,
match: MatchIs,
key: []string{"[email protected]"},
},
AddressPart: All,
Field: []string{"from"},
Key: []string{"[email protected]"},
},
Block: []Cmd{
CmdFileInto{Mailbox: "hell"},
Expand Down
69 changes: 67 additions & 2 deletions interp/matchertest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package interp

import (
"fmt"
"strconv"
)

// matcherTest contains code shared between tests
Expand All @@ -11,9 +12,10 @@ import (
type matcherTest struct {
comparator Comparator
match Match
relational Relational
key []string

// Used for keys without
// Used for keys without variables.
keyCompiled []CompiledMatcher

matchCnt int
Expand All @@ -37,6 +39,7 @@ func (t *matcherTest) addSpecTags(s *Spec) *Spec {
MatchStr: func(val []string) {
t.comparator = Comparator(val[0])
},
NoVariables: true,
}
s.Tags["is"] = SpecTag{
MatchBool: func() {
Expand All @@ -56,6 +59,28 @@ func (t *matcherTest) addSpecTags(s *Spec) *Spec {
t.matchCnt++
},
}
s.Tags["value"] = SpecTag{
NeedsValue: true,
MinStrCount: 1,
MaxStrCount: 1,
NoVariables: true,
MatchStr: func(val []string) {
t.match = MatchValue
t.matchCnt++
t.relational = Relational(val[0])
},
}
s.Tags["count"] = SpecTag{
NeedsValue: true,
MinStrCount: 1,
MaxStrCount: 1,
NoVariables: true,
MatchStr: func(val []string) {
t.match = MatchCount
t.matchCnt++
t.relational = Relational(val[0])
},
}
return s
}

Expand All @@ -66,6 +91,19 @@ func (t *matcherTest) setKey(s *Script, k []string) error {
return fmt.Errorf("multiple match-types are not allowed")
}

if t.match == MatchCount || t.match == MatchValue {
if !s.RequiresExtension("relational") {
return fmt.Errorf("missing require 'relational'")
}
switch t.relational {
case RelGreaterThan, RelGreaterOrEqual,
RelLessThan, RelLessOrEqual, RelEqual,
RelNotEqual:
default:
return fmt.Errorf("unknown relational operator: %v", t.relational)
}
}

caseFold := false
octet := false
switch t.comparator {
Expand Down Expand Up @@ -96,9 +134,36 @@ func (t *matcherTest) setKey(s *Script, k []string) error {
}
}

if t.match == MatchCount && t.comparator != ComparatorASCIINumeric {
return fmt.Errorf("non-numeric comparators cannot be used with :count")
}

return nil
}

func (t *matcherTest) isCount() bool {
return t.match == MatchCount
}

func (t *matcherTest) countMatches(d *RuntimeData, value uint64) bool {
if !t.isCount() {
panic("countMatches can be called only with MatchCount matcher")
}

for _, k := range t.key {
kNum, err := strconv.ParseUint(expandVars(d, k), 10, 64)
if err != nil {
continue
}

if t.relational.CompareUint64(value, kNum) {
return true
}
}

return false
}

func (t *matcherTest) tryMatch(d *RuntimeData, source string) (bool, error) {
for i, key := range t.key {
var (
Expand All @@ -110,7 +175,7 @@ func (t *matcherTest) tryMatch(d *RuntimeData, source string) (bool, error) {
ok, matches, err = t.keyCompiled[i](source)
} else {
key = expandVars(d, key)
ok, matches, err = testString(t.comparator, t.match, source, expandVars(d, key))
ok, matches, err = testString(t.comparator, t.match, t.relational, source, expandVars(d, key))
}
if err != nil {
return false, err
Expand Down
94 changes: 94 additions & 0 deletions interp/relational.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package interp

type Relational string

const (
RelGreaterThan Relational = "gt"
RelGreaterOrEqual Relational = "ge"
RelLessThan Relational = "lt"
RelLessOrEqual Relational = "le"
RelEqual Relational = "eq"
RelNotEqual Relational = "ne"
)

func (r Relational) CompareString(lhs, rhs string) bool {
switch r {
case RelGreaterThan:
return lhs > rhs
case RelGreaterOrEqual:
return lhs >= rhs
case RelLessThan:
return lhs < rhs
case RelLessOrEqual:
return lhs <= rhs
case RelEqual:
return lhs == rhs
case RelNotEqual:
return lhs != rhs
}
return false
}

func (r Relational) CompareUint64(lhs, rhs uint64) bool {
switch r {
case RelGreaterThan:
return lhs > rhs
case RelGreaterOrEqual:
return lhs >= rhs
case RelLessThan:
return lhs < rhs
case RelLessOrEqual:
return lhs <= rhs
case RelEqual:
return lhs == rhs
case RelNotEqual:
return lhs != rhs
}
return false
}

func (r Relational) CompareNumericValue(lhs, rhs *uint64) bool {
// https://www.rfc-editor.org/rfc/rfc4790.html#section-9.1
// nil (string not starting with a digit)
// represents positive infinity. inf == inf. inf > any integer.

switch r {
case RelGreaterThan:
if lhs == nil {
if rhs == nil {
return false
}
return true
}
if rhs == nil {
return false
}
return *lhs > *rhs
case RelGreaterOrEqual:
return !RelLessThan.CompareNumericValue(lhs, rhs)
case RelLessThan:
if rhs == nil {
if lhs == nil {
return false
}
return true
}
if lhs == nil {
return false
}
return *lhs < *rhs
case RelLessOrEqual:
return !RelGreaterThan.CompareNumericValue(lhs, rhs)
case RelEqual:
if lhs == nil && rhs == nil {
return true
}
if lhs != nil && rhs != nil {
return *lhs == *rhs
}
return false
case RelNotEqual:
return !RelEqual.CompareNumericValue(lhs, rhs)
}
return false
}
Loading

0 comments on commit 72d6b00

Please sign in to comment.