mirror of
https://github.com/safing/portmaster
synced 2025-04-11 06:29:11 +00:00
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
350 lines
8 KiB
Go
350 lines
8 KiB
Go
package query
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type snippet struct {
|
|
text string
|
|
globalPosition int
|
|
}
|
|
|
|
// ParseQuery parses a plaintext query. Special characters (that must be escaped with a '\') are: `\()` and any whitespaces.
|
|
//
|
|
//nolint:gocognit
|
|
func ParseQuery(query string) (*Query, error) {
|
|
snippets, err := extractSnippets(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snippetsPos := 0
|
|
|
|
getSnippet := func() (*snippet, error) {
|
|
// order is important, as parseAndOr will always consume one additional snippet.
|
|
snippetsPos++
|
|
if snippetsPos > len(snippets) {
|
|
return nil, fmt.Errorf("unexpected end at position %d", len(query))
|
|
}
|
|
return snippets[snippetsPos-1], nil
|
|
}
|
|
remainingSnippets := func() int {
|
|
return len(snippets) - snippetsPos
|
|
}
|
|
|
|
// check for query word
|
|
queryWord, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if queryWord.text != "query" {
|
|
return nil, errors.New("queries must start with \"query\"")
|
|
}
|
|
|
|
// get prefix
|
|
prefix, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
q := New(prefix.text)
|
|
|
|
for remainingSnippets() > 0 {
|
|
command, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch command.text {
|
|
case "where":
|
|
if q.where != nil {
|
|
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
|
}
|
|
|
|
// parse conditions
|
|
condition, err := parseAndOr(getSnippet, remainingSnippets, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// go one back, as parseAndOr had to check if its done
|
|
snippetsPos--
|
|
|
|
q.Where(condition)
|
|
case "orderby":
|
|
if q.orderBy != "" {
|
|
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
|
}
|
|
|
|
orderBySnippet, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q.OrderBy(orderBySnippet.text)
|
|
case "limit":
|
|
if q.limit != 0 {
|
|
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
|
}
|
|
|
|
limitSnippet, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
limit, err := strconv.ParseUint(limitSnippet.text, 10, 31)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse integer (%s) at position %d", limitSnippet.text, limitSnippet.globalPosition)
|
|
}
|
|
|
|
q.Limit(int(limit))
|
|
case "offset":
|
|
if q.offset != 0 {
|
|
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
|
}
|
|
|
|
offsetSnippet, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
offset, err := strconv.ParseUint(offsetSnippet.text, 10, 31)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse integer (%s) at position %d", offsetSnippet.text, offsetSnippet.globalPosition)
|
|
}
|
|
|
|
q.Offset(int(offset))
|
|
default:
|
|
return nil, fmt.Errorf("unknown clause \"%s\" at position %d", command.text, command.globalPosition)
|
|
}
|
|
}
|
|
|
|
return q.Check()
|
|
}
|
|
|
|
func extractSnippets(text string) (snippets []*snippet, err error) {
|
|
skip := false
|
|
start := -1
|
|
inParenthesis := false
|
|
var pos int
|
|
var char rune
|
|
|
|
for pos, char = range text {
|
|
|
|
// skip
|
|
if skip {
|
|
skip = false
|
|
continue
|
|
}
|
|
if char == '\\' {
|
|
skip = true
|
|
}
|
|
|
|
// wait for parenthesis to be overs
|
|
if inParenthesis {
|
|
if char == '"' {
|
|
snippets = append(snippets, &snippet{
|
|
text: prepToken(text[start+1 : pos]),
|
|
globalPosition: start + 1,
|
|
})
|
|
start = -1
|
|
inParenthesis = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
// handle segments
|
|
switch char {
|
|
case '\t', '\n', '\r', ' ', '(', ')':
|
|
if start >= 0 {
|
|
snippets = append(snippets, &snippet{
|
|
text: prepToken(text[start:pos]),
|
|
globalPosition: start + 1,
|
|
})
|
|
start = -1
|
|
}
|
|
default:
|
|
if start == -1 {
|
|
start = pos
|
|
}
|
|
}
|
|
|
|
// handle special segment characters
|
|
switch char {
|
|
case '(', ')':
|
|
snippets = append(snippets, &snippet{
|
|
text: text[pos : pos+1],
|
|
globalPosition: pos + 1,
|
|
})
|
|
case '"':
|
|
if start < pos {
|
|
return nil, fmt.Errorf("parenthesis ('\"') may not be used within words, please escape with '\\' (position: %d)", pos+1)
|
|
}
|
|
inParenthesis = true
|
|
}
|
|
|
|
}
|
|
|
|
// add last
|
|
if start >= 0 {
|
|
snippets = append(snippets, &snippet{
|
|
text: prepToken(text[start : pos+1]),
|
|
globalPosition: start + 1,
|
|
})
|
|
}
|
|
|
|
return snippets, nil
|
|
}
|
|
|
|
//nolint:gocognit
|
|
func parseAndOr(getSnippet func() (*snippet, error), remainingSnippets func() int, rootCondition bool) (Condition, error) {
|
|
var (
|
|
isOr = false
|
|
typeSet = false
|
|
wrapInNot = false
|
|
expectingMore = true
|
|
conditions []Condition
|
|
)
|
|
|
|
for {
|
|
if !expectingMore && rootCondition && remainingSnippets() == 0 {
|
|
// advance snippetsPos by one, as it will be set back by 1
|
|
_, _ = getSnippet()
|
|
if len(conditions) == 1 {
|
|
return conditions[0], nil
|
|
}
|
|
if isOr {
|
|
return Or(conditions...), nil
|
|
}
|
|
return And(conditions...), nil
|
|
}
|
|
|
|
firstSnippet, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !expectingMore && rootCondition {
|
|
switch firstSnippet.text {
|
|
case "orderby", "limit", "offset":
|
|
if len(conditions) == 1 {
|
|
return conditions[0], nil
|
|
}
|
|
if isOr {
|
|
return Or(conditions...), nil
|
|
}
|
|
return And(conditions...), nil
|
|
}
|
|
}
|
|
|
|
switch firstSnippet.text {
|
|
case "(":
|
|
condition, err := parseAndOr(getSnippet, remainingSnippets, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wrapInNot {
|
|
conditions = append(conditions, Not(condition))
|
|
wrapInNot = false
|
|
} else {
|
|
conditions = append(conditions, condition)
|
|
}
|
|
expectingMore = true
|
|
case ")":
|
|
if len(conditions) == 1 {
|
|
return conditions[0], nil
|
|
}
|
|
if isOr {
|
|
return Or(conditions...), nil
|
|
}
|
|
return And(conditions...), nil
|
|
case "and":
|
|
if typeSet && isOr {
|
|
return nil, fmt.Errorf("you may not mix \"and\" and \"or\" (position: %d)", firstSnippet.globalPosition)
|
|
}
|
|
isOr = false
|
|
typeSet = true
|
|
expectingMore = true
|
|
case "or":
|
|
if typeSet && !isOr {
|
|
return nil, fmt.Errorf("you may not mix \"and\" and \"or\" (position: %d)", firstSnippet.globalPosition)
|
|
}
|
|
isOr = true
|
|
typeSet = true
|
|
expectingMore = true
|
|
case "not":
|
|
wrapInNot = true
|
|
expectingMore = true
|
|
default:
|
|
condition, err := parseCondition(firstSnippet, getSnippet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wrapInNot {
|
|
conditions = append(conditions, Not(condition))
|
|
wrapInNot = false
|
|
} else {
|
|
conditions = append(conditions, condition)
|
|
}
|
|
expectingMore = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseCondition(firstSnippet *snippet, getSnippet func() (*snippet, error)) (Condition, error) {
|
|
wrapInNot := false
|
|
|
|
// get operator name
|
|
opName, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// negate?
|
|
if opName.text == "not" {
|
|
wrapInNot = true
|
|
opName, err = getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// get operator
|
|
operator, ok := operatorNames[opName.text]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown operator at position %d", opName.globalPosition)
|
|
}
|
|
|
|
// don't need a value for "exists"
|
|
if operator == Exists {
|
|
if wrapInNot {
|
|
return Not(Where(firstSnippet.text, operator, nil)), nil
|
|
}
|
|
return Where(firstSnippet.text, operator, nil), nil
|
|
}
|
|
|
|
// get value
|
|
value, err := getSnippet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wrapInNot {
|
|
return Not(Where(firstSnippet.text, operator, value.text)), nil
|
|
}
|
|
return Where(firstSnippet.text, operator, value.text), nil
|
|
}
|
|
|
|
var escapeReplacer = regexp.MustCompile(`\\([^\\])`)
|
|
|
|
// prepToken removes surrounding parenthesis and escape characters.
|
|
func prepToken(text string) string {
|
|
return escapeReplacer.ReplaceAllString(strings.Trim(text, "\""), "$1")
|
|
}
|
|
|
|
// escapeString correctly escapes a snippet for printing.
|
|
func escapeString(token string) string {
|
|
// check if token contains characters that need to be escaped
|
|
if strings.ContainsAny(token, "()\"\\\t\r\n ") {
|
|
// put the token in parenthesis and only escape \ and "
|
|
return fmt.Sprintf("\"%s\"", strings.ReplaceAll(token, "\"", "\\\""))
|
|
}
|
|
return token
|
|
}
|