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 }