Skip to content

Commit

Permalink
interp: Implement "matches"
Browse files Browse the repository at this point in the history
  • Loading branch information
foxcpp committed Jan 28, 2024
1 parent 361efc3 commit 2b1f0a2
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 150 deletions.
155 changes: 5 additions & 150 deletions interp/match.go
Original file line number Diff line number Diff line change
@@ -1,156 +1,11 @@
package interp

import (
"errors"
"fmt"
"net/mail"
"strconv"
"strings"
"unicode"
)
import "github.com/foxcpp/go-sieve/interp/match"

type Match string

const (
MatchContains Match = "contains"
MatchIs Match = "is"
MatchMatches Match = "matches"
)

type Comparator string

const (
ComparatorOctet Comparator = "i;octet"
ComparatorASCIICaseMap Comparator = "i;ascii-casemap"
ComparatorASCIINumeric Comparator = "i;ascii-numeric"
ComparatorUnicodeCaseMap Comparator = "i;unicode-casemap"
)

type AddressPart string

const (
LocalPart AddressPart = "localpart"
Domain AddressPart = "domain"
All AddressPart = "all"
)

func split(addr string) (mailbox, domain string, err error) {
if strings.EqualFold(addr, "postmaster") {
return addr, "", nil
}

indx := strings.LastIndexByte(addr, '@')
if indx == -1 {
return "", "", errors.New("address: missing at-sign")
}
mailbox = addr[:indx]
domain = addr[indx+1:]
if mailbox == "" {
return "", "", errors.New("address: empty local-part")
}
if domain == "" {
return "", "", errors.New("address: empty domain")
}
return
func matchOctet(pattern, value string) (bool, error) {
return match.Match([]byte(pattern), []byte(value))
}

var ErrComparatorMatchUnsupported = fmt.Errorf("match-comparator combination not supported")

func matchString(pattern, key string) (bool, error) {
// FIXME: This does not account for comparator differences.
panic("match is not implemented")
}

func numericValue(s string) *uint64 {
if len(s) == 0 {
return nil
}
runes := []rune(s)
if !unicode.IsDigit(runes[0]) {
return nil
}
var sl string
for i, r := range runes {
if !unicode.IsDigit(r) {
sl = string(runes[:i])
}
}
digit, _ := strconv.ParseUint(sl, 10, 64)
return &digit
}

func testString(comparator Comparator, match Match, value, key string) (bool, error) {
switch comparator {
case ComparatorOctet:
switch match {
case MatchContains:
return strings.Contains(value, key), nil
case MatchIs:
return value == key, nil
case MatchMatches:
return matchString(key, value)
}
case ComparatorASCIINumeric:
switch match {
case MatchContains:
return false, ErrComparatorMatchUnsupported
case MatchIs:
lhsNum := numericValue(value)
rhsNum := numericValue(key)
if lhsNum == nil || rhsNum == nil {
return false, nil
}
return *lhsNum == *rhsNum, nil
case MatchMatches:
return false, ErrComparatorMatchUnsupported
}
case ComparatorASCIICaseMap:
// FIXME: Case-fold ASCII only.
fallthrough
case ComparatorUnicodeCaseMap:
value = strings.ToLower(value)
key = strings.ToLower(key)
switch match {
case MatchContains:
return strings.Contains(value, key), nil
case MatchIs:
return value == key, nil
case MatchMatches:
return matchString(value, key)
}
}
return false, nil
}

func testAddress(part AddressPart, comparator Comparator, match Match, headerVal []*mail.Address, addrValue string) (bool, error) {
for _, addr := range headerVal {
var valueToCompare string
if addr.Address != "" {
switch part {
case LocalPart:
localPart, _, err := split(addr.Address)
if err != nil {
continue
}
valueToCompare = localPart
case Domain:
_, domain, err := split(addr.Address)
if err != nil {
continue
}
valueToCompare = domain
case All:
valueToCompare = addr.Address
}
}

ok, err := testString(comparator, match, valueToCompare, addrValue)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
func matchUnicode(pattern, value string) (bool, error) {
return match.Match([]rune(pattern), []rune(value))
}
140 changes: 140 additions & 0 deletions interp/match/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package match

import (
"errors"
)

// ErrBadPattern indicates a pattern was malformed.
var ErrBadPattern = errors.New("syntax error in pattern")

// Match is a simplified version of path.Match that removes support
// for character classes and operates on already decoded input ([]byte or []rune).
func Match[T rune | byte](pattern, name []T) (matched bool, err error) {
Pattern:
for len(pattern) > 0 {
var star bool
var chunk []T
star, chunk, pattern = scanChunk(pattern)
if star && len(chunk) == 0 {
return true, nil
}
// Look for match at current position.
t, ok, err := matchChunk(chunk, name)
// if we're the last chunk, make sure we've exhausted the name
// otherwise we'll give a false result even if we could still match
// using the star
if ok && (len(t) == 0 || len(pattern) > 0) {
name = t
continue
}
if err != nil {
return false, err
}
if star {
// Look for match skipping i+1 bytes.
for i := 0; i < len(name); i++ {
t, ok, err := matchChunk(chunk, name[i+1:])
if ok {
// if we're the last chunk, make sure we exhausted the name
if len(pattern) == 0 && len(t) > 0 {
continue
}
name = t
continue Pattern
}
if err != nil {
return false, err
}
}
}
// Before returning false with no error,
// check that the remainder of the pattern is syntactically valid.
for len(pattern) > 0 {
_, chunk, pattern = scanChunk(pattern)
if _, _, err := matchChunk(chunk, []T{}); err != nil {
return false, err
}
}
return false, nil
}
return len(name) == 0, nil
}

// scanChunk gets the next segment of pattern, which is a non-star string
// possibly preceded by a star.
func scanChunk[T rune | byte](pattern []T) (star bool, chunk, rest []T) {
for len(pattern) > 0 && pattern[0] == '*' {
pattern = pattern[1:]
star = true
}
inrange := false
var i int
Scan:
for i = 0; i < len(pattern); i++ {
switch pattern[i] {
case '\\':
// error check handled in matchChunk: bad pattern.
if i+1 < len(pattern) {
i++
}
case '[':
inrange = true
case ']':
inrange = false
case '*':
if !inrange {
break Scan
}
}
}
return star, pattern[0:i], pattern[i:]
}

// matchChunk checks whether chunk matches the beginning of s.
// If so, it returns the remainder of s (after the match).
// Chunk is all single-character operators: literals, char classes, and ?.
func matchChunk[T rune | byte](chunk, s []T) (rest []T, ok bool, err error) {
// failed records whether the match has failed.
// After the match fails, the loop continues on processing chunk,
// checking that the pattern is well-formed but no longer reading s.
failed := false
for len(chunk) > 0 {
if !failed && len(s) == 0 {
failed = true
}
switch chunk[0] {
case '?':
if !failed {
if s[0] == '/' {
failed = true
}
s = s[1:]
}
chunk = chunk[1:]

case '\\':
chunk = chunk[1:]
if len(chunk) == 0 {
return []T{}, false, ErrBadPattern
}
fallthrough

default:
if !failed {
if chunk[0] != s[0] {
failed = true
}
s = s[1:]
}
chunk = chunk[1:]
}
}
if failed {
return []T{}, false, nil
}
return s, true, nil
}
Loading

0 comments on commit 2b1f0a2

Please sign in to comment.