blob: d1d8c87dc8410464b1a1a963525d87554c3b30c2 [file] [log] [blame]
// Copyright 2017 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package expr provides support for evaluating boolean expressions.
package expr
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"regexp"
"strconv"
"strings"
)
// Expr holds a parsed boolean expression that matches some combination of attributes.
//
// Expressions are supplied as strings consisting of the following tokens:
//
// - Attributes, either as bare identifiers (only if compliant with
// https://golang.org/ref/spec#Identifiers) or as double-quoted strings
// (in which '*' characters are interpreted as wildcards)
// - Binary operators: && (and), || (or)
// - Unary operator: ! (not)
// - Grouping: (, )
//
// The expression syntax is conveniently a subset of Go's syntax, so Go's parser and ast
// packages are used to convert the initial expression into a binary expression tree.
//
// After an Expr object is created from a string expression, it can be asked if
// it is satisfied by a supplied set of attributes.
type Expr struct {
root ast.Expr
}
// exprValidator is used to validate that a parsed Go expression is a
// valid boolean expression. It implements the ast.Visitor interface.
type exprValidator struct {
err error
}
// setErr stores a formatted error in ev's err field (if not already set) and
// returns nil to instruct ast.Walk to stop walking the expression.
func (ev *exprValidator) setErr(format string, args ...interface{}) ast.Visitor {
if ev.err == nil {
ev.err = fmt.Errorf(format, args...)
}
return nil
}
// Visit returns itself if n is a valid node in a boolean expression or sets
// ev's err field and returns nil if it isn't.
func (ev *exprValidator) Visit(n ast.Node) ast.Visitor {
// ast.Walk calls Visit(nil) after visiting non-nil children.
if n == nil {
return nil
}
switch v := n.(type) {
case *ast.BinaryExpr:
if v.Op != token.LAND && v.Op != token.LOR {
return ev.setErr("invalid binary operator %q", v.Op)
}
case *ast.ParenExpr:
case *ast.UnaryExpr:
if v.Op != token.NOT {
return ev.setErr("invalid unary operator %q", v.Op)
}
case *ast.Ident:
case *ast.BasicLit:
if v.Kind != token.STRING {
return ev.setErr("non-string literal %q", v.Value)
}
default:
return ev.setErr("invalid node of type %T", v)
}
return ev
}
// New parses and validates boolean expression s, returning an Expr object
// that can be used to test whether the expression is satisfied by different
// sets of attributes.
func New(s string) (*Expr, error) {
root, err := parser.ParseExpr(s)
if err != nil {
return nil, err
}
v := exprValidator{}
ast.Walk(&v, root)
return &Expr{root}, v.err
}
// Matches returns true if the expression is satisfied by attributes attrs.
func (e *Expr) Matches(attrs []string) bool {
am := make(map[string]struct{}, len(attrs))
for _, a := range attrs {
am[a] = struct{}{}
}
return exprTrue(e.root, am)
}
// exprTrue returns true if e is satisfied by attributes attrs.
func exprTrue(e ast.Expr, attrs map[string]struct{}) bool {
switch v := e.(type) {
case *ast.BinaryExpr:
switch v.Op {
case token.LAND:
return exprTrue(v.X, attrs) && exprTrue(v.Y, attrs)
case token.LOR:
return exprTrue(v.X, attrs) || exprTrue(v.Y, attrs)
}
case *ast.ParenExpr:
return exprTrue(v.X, attrs)
case *ast.UnaryExpr:
switch v.Op {
case token.NOT:
return !exprTrue(v.X, attrs)
}
case *ast.Ident:
return hasAttr(attrs, v.Name)
case *ast.BasicLit:
switch v.Kind {
case token.STRING:
// Strip doublequotes.
str, err := strconv.Unquote(v.Value)
if err != nil {
return false
}
return hasAttr(attrs, str)
}
}
return false
}
func hasAttr(attrs map[string]struct{}, want string) bool {
if !strings.Contains(want, "*") {
_, ok := attrs[want]
return ok
}
// The pattern looks like a glob. Temporarily replace asterisks with zero bytes
// so we can escape other chars that have special meanings in regular expressions.
want = strings.Replace(want, "*", "\000", -1)
want = regexp.QuoteMeta(want)
want = strings.Replace(want, "\000", ".*", -1)
want = "^" + want + "$"
re, err := regexp.Compile(want)
if err != nil {
return false
}
for attr := range attrs {
if re.MatchString(attr) {
return true
}
}
return false
}