github.com/safing/portbase@v0.19.5/database/query/parser.go (about)

     1  package query
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  )
    10  
    11  type snippet struct {
    12  	text           string
    13  	globalPosition int
    14  }
    15  
    16  // ParseQuery parses a plaintext query. Special characters (that must be escaped with a '\') are: `\()` and any whitespaces.
    17  //
    18  //nolint:gocognit
    19  func ParseQuery(query string) (*Query, error) {
    20  	snippets, err := extractSnippets(query)
    21  	if err != nil {
    22  		return nil, err
    23  	}
    24  	snippetsPos := 0
    25  
    26  	getSnippet := func() (*snippet, error) {
    27  		// order is important, as parseAndOr will always consume one additional snippet.
    28  		snippetsPos++
    29  		if snippetsPos > len(snippets) {
    30  			return nil, fmt.Errorf("unexpected end at position %d", len(query))
    31  		}
    32  		return snippets[snippetsPos-1], nil
    33  	}
    34  	remainingSnippets := func() int {
    35  		return len(snippets) - snippetsPos
    36  	}
    37  
    38  	// check for query word
    39  	queryWord, err := getSnippet()
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	if queryWord.text != "query" {
    44  		return nil, errors.New("queries must start with \"query\"")
    45  	}
    46  
    47  	// get prefix
    48  	prefix, err := getSnippet()
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	q := New(prefix.text)
    53  
    54  	for remainingSnippets() > 0 {
    55  		command, err := getSnippet()
    56  		if err != nil {
    57  			return nil, err
    58  		}
    59  
    60  		switch command.text {
    61  		case "where":
    62  			if q.where != nil {
    63  				return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
    64  			}
    65  
    66  			// parse conditions
    67  			condition, err := parseAndOr(getSnippet, remainingSnippets, true)
    68  			if err != nil {
    69  				return nil, err
    70  			}
    71  			// go one back, as parseAndOr had to check if its done
    72  			snippetsPos--
    73  
    74  			q.Where(condition)
    75  		case "orderby":
    76  			if q.orderBy != "" {
    77  				return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
    78  			}
    79  
    80  			orderBySnippet, err := getSnippet()
    81  			if err != nil {
    82  				return nil, err
    83  			}
    84  
    85  			q.OrderBy(orderBySnippet.text)
    86  		case "limit":
    87  			if q.limit != 0 {
    88  				return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
    89  			}
    90  
    91  			limitSnippet, err := getSnippet()
    92  			if err != nil {
    93  				return nil, err
    94  			}
    95  			limit, err := strconv.ParseUint(limitSnippet.text, 10, 31)
    96  			if err != nil {
    97  				return nil, fmt.Errorf("could not parse integer (%s) at position %d", limitSnippet.text, limitSnippet.globalPosition)
    98  			}
    99  
   100  			q.Limit(int(limit))
   101  		case "offset":
   102  			if q.offset != 0 {
   103  				return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
   104  			}
   105  
   106  			offsetSnippet, err := getSnippet()
   107  			if err != nil {
   108  				return nil, err
   109  			}
   110  			offset, err := strconv.ParseUint(offsetSnippet.text, 10, 31)
   111  			if err != nil {
   112  				return nil, fmt.Errorf("could not parse integer (%s) at position %d", offsetSnippet.text, offsetSnippet.globalPosition)
   113  			}
   114  
   115  			q.Offset(int(offset))
   116  		default:
   117  			return nil, fmt.Errorf("unknown clause \"%s\" at position %d", command.text, command.globalPosition)
   118  		}
   119  	}
   120  
   121  	return q.Check()
   122  }
   123  
   124  func extractSnippets(text string) (snippets []*snippet, err error) {
   125  	skip := false
   126  	start := -1
   127  	inParenthesis := false
   128  	var pos int
   129  	var char rune
   130  
   131  	for pos, char = range text {
   132  
   133  		// skip
   134  		if skip {
   135  			skip = false
   136  			continue
   137  		}
   138  		if char == '\\' {
   139  			skip = true
   140  		}
   141  
   142  		// wait for parenthesis to be overs
   143  		if inParenthesis {
   144  			if char == '"' {
   145  				snippets = append(snippets, &snippet{
   146  					text:           prepToken(text[start+1 : pos]),
   147  					globalPosition: start + 1,
   148  				})
   149  				start = -1
   150  				inParenthesis = false
   151  			}
   152  			continue
   153  		}
   154  
   155  		// handle segments
   156  		switch char {
   157  		case '\t', '\n', '\r', ' ', '(', ')':
   158  			if start >= 0 {
   159  				snippets = append(snippets, &snippet{
   160  					text:           prepToken(text[start:pos]),
   161  					globalPosition: start + 1,
   162  				})
   163  				start = -1
   164  			}
   165  		default:
   166  			if start == -1 {
   167  				start = pos
   168  			}
   169  		}
   170  
   171  		// handle special segment characters
   172  		switch char {
   173  		case '(', ')':
   174  			snippets = append(snippets, &snippet{
   175  				text:           text[pos : pos+1],
   176  				globalPosition: pos + 1,
   177  			})
   178  		case '"':
   179  			if start < pos {
   180  				return nil, fmt.Errorf("parenthesis ('\"') may not be used within words, please escape with '\\' (position: %d)", pos+1)
   181  			}
   182  			inParenthesis = true
   183  		}
   184  
   185  	}
   186  
   187  	// add last
   188  	if start >= 0 {
   189  		snippets = append(snippets, &snippet{
   190  			text:           prepToken(text[start : pos+1]),
   191  			globalPosition: start + 1,
   192  		})
   193  	}
   194  
   195  	return snippets, nil
   196  }
   197  
   198  //nolint:gocognit
   199  func parseAndOr(getSnippet func() (*snippet, error), remainingSnippets func() int, rootCondition bool) (Condition, error) {
   200  	var (
   201  		isOr          = false
   202  		typeSet       = false
   203  		wrapInNot     = false
   204  		expectingMore = true
   205  		conditions    []Condition
   206  	)
   207  
   208  	for {
   209  		if !expectingMore && rootCondition && remainingSnippets() == 0 {
   210  			// advance snippetsPos by one, as it will be set back by 1
   211  			_, _ = getSnippet()
   212  			if len(conditions) == 1 {
   213  				return conditions[0], nil
   214  			}
   215  			if isOr {
   216  				return Or(conditions...), nil
   217  			}
   218  			return And(conditions...), nil
   219  		}
   220  
   221  		firstSnippet, err := getSnippet()
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  
   226  		if !expectingMore && rootCondition {
   227  			switch firstSnippet.text {
   228  			case "orderby", "limit", "offset":
   229  				if len(conditions) == 1 {
   230  					return conditions[0], nil
   231  				}
   232  				if isOr {
   233  					return Or(conditions...), nil
   234  				}
   235  				return And(conditions...), nil
   236  			}
   237  		}
   238  
   239  		switch firstSnippet.text {
   240  		case "(":
   241  			condition, err := parseAndOr(getSnippet, remainingSnippets, false)
   242  			if err != nil {
   243  				return nil, err
   244  			}
   245  			if wrapInNot {
   246  				conditions = append(conditions, Not(condition))
   247  				wrapInNot = false
   248  			} else {
   249  				conditions = append(conditions, condition)
   250  			}
   251  			expectingMore = true
   252  		case ")":
   253  			if len(conditions) == 1 {
   254  				return conditions[0], nil
   255  			}
   256  			if isOr {
   257  				return Or(conditions...), nil
   258  			}
   259  			return And(conditions...), nil
   260  		case "and":
   261  			if typeSet && isOr {
   262  				return nil, fmt.Errorf("you may not mix \"and\" and \"or\" (position: %d)", firstSnippet.globalPosition)
   263  			}
   264  			isOr = false
   265  			typeSet = true
   266  			expectingMore = true
   267  		case "or":
   268  			if typeSet && !isOr {
   269  				return nil, fmt.Errorf("you may not mix \"and\" and \"or\" (position: %d)", firstSnippet.globalPosition)
   270  			}
   271  			isOr = true
   272  			typeSet = true
   273  			expectingMore = true
   274  		case "not":
   275  			wrapInNot = true
   276  			expectingMore = true
   277  		default:
   278  			condition, err := parseCondition(firstSnippet, getSnippet)
   279  			if err != nil {
   280  				return nil, err
   281  			}
   282  			if wrapInNot {
   283  				conditions = append(conditions, Not(condition))
   284  				wrapInNot = false
   285  			} else {
   286  				conditions = append(conditions, condition)
   287  			}
   288  			expectingMore = false
   289  		}
   290  	}
   291  }
   292  
   293  func parseCondition(firstSnippet *snippet, getSnippet func() (*snippet, error)) (Condition, error) {
   294  	wrapInNot := false
   295  
   296  	// get operator name
   297  	opName, err := getSnippet()
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	// negate?
   302  	if opName.text == "not" {
   303  		wrapInNot = true
   304  		opName, err = getSnippet()
   305  		if err != nil {
   306  			return nil, err
   307  		}
   308  	}
   309  
   310  	// get operator
   311  	operator, ok := operatorNames[opName.text]
   312  	if !ok {
   313  		return nil, fmt.Errorf("unknown operator at position %d", opName.globalPosition)
   314  	}
   315  
   316  	// don't need a value for "exists"
   317  	if operator == Exists {
   318  		if wrapInNot {
   319  			return Not(Where(firstSnippet.text, operator, nil)), nil
   320  		}
   321  		return Where(firstSnippet.text, operator, nil), nil
   322  	}
   323  
   324  	// get value
   325  	value, err := getSnippet()
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	if wrapInNot {
   330  		return Not(Where(firstSnippet.text, operator, value.text)), nil
   331  	}
   332  	return Where(firstSnippet.text, operator, value.text), nil
   333  }
   334  
   335  var escapeReplacer = regexp.MustCompile(`\\([^\\])`)
   336  
   337  // prepToken removes surrounding parenthesis and escape characters.
   338  func prepToken(text string) string {
   339  	return escapeReplacer.ReplaceAllString(strings.Trim(text, "\""), "$1")
   340  }
   341  
   342  // escapeString correctly escapes a snippet for printing.
   343  func escapeString(token string) string {
   344  	// check if token contains characters that need to be escaped
   345  	if strings.ContainsAny(token, "()\"\\\t\r\n ") {
   346  		// put the token in parenthesis and only escape \ and "
   347  		return fmt.Sprintf("\"%s\"", strings.ReplaceAll(token, "\"", "\\\""))
   348  	}
   349  	return token
   350  }