From d6ef054a4e555955c3bba90390f5a406fda88382 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 27 Dec 2025 16:27:46 +0100 Subject: [PATCH] perf(rule_engine): Move to iterative greedy wildcard matching By switching from recursive backtracking to an iterative greedy matcher, we've eliminated: - all the recursion overhead. - repeated slice copies for every recursive call. - exponential branching when * appears in the pattern. The matcher is now linear-time in the length of the pattern and string and introduces the ASCII-fast path with UTF-8 fallback only when needed. --- pkg/util/wildcard/wildcard.go | 118 +++++++++++++++++++---------- pkg/util/wildcard/wildcard_test.go | 32 ++++++-- 2 files changed, 101 insertions(+), 49 deletions(-) diff --git a/pkg/util/wildcard/wildcard.go b/pkg/util/wildcard/wildcard.go index 86d88db51..b0f7d8f70 100644 --- a/pkg/util/wildcard/wildcard.go +++ b/pkg/util/wildcard/wildcard.go @@ -1,53 +1,89 @@ /* - * MinIO Cloud Storage, (C) 2015, 2016 MinIO, Inc. + * Copyright 2019-present by Nedim Sabic + * http://rabbitstack.github.io + * All Rights Reserved. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * http://www.apache.org/licenses/LICENSE-2.0 */ package wildcard -// Match - finds whether the text matches/satisfies the pattern string. -// supports '*' and '?' wildcards in the pattern string. -// unlike path.Match(), considers a path as a flat name space while matching the pattern. -// The difference is illustrated in the example here https://play.golang.org/p/Ega9qgD4Qz . -func Match(pattern, name string) (matched bool) { - if pattern == "" { - return name == pattern - } - if pattern == "*" { - return true - } - // Does extended wildcard '*' and '?' match? - return deepMatchRune(name, pattern, false) -} +import "unicode/utf8" -func deepMatchRune(s, pattern string, simple bool) bool { - for len(pattern) > 0 { - switch pattern[0] { - default: - if len(s) == 0 || s[0] != pattern[0] { - return false - } - case '?': - if len(s) == 0 && !simple { - return false +// Match performs ASCII-first, iterative wildcard matching with UTF-8 fallback. +// It supports '*' and '?' wildcards in the pattern string. +func Match(pattern, str string) bool { + slen := len(str) + plen := len(pattern) + + var p, s int + wildcardIdx, matchIdx := -1, 0 + + for s < slen { + if p < plen { + pb := pattern[p] + + switch pb { + case '?': + // match exactly one character + if str[s] < utf8.RuneSelf && pb < utf8.RuneSelf { + p++ + s++ + } else { + _, psize := utf8.DecodeRuneInString(pattern[p:]) + _, ssize := utf8.DecodeRuneInString(str[s:]) + p += psize + s += ssize + } + continue + + case '*': + // record wildcard position + wildcardIdx = p + matchIdx = s + p++ + continue + + default: + // literal match + if pb < utf8.RuneSelf && str[s] < utf8.RuneSelf { + if pb == str[s] { + p++ + s++ + continue + } + } else { + pr, psize := utf8.DecodeRuneInString(pattern[p:]) + sr, ssize := utf8.DecodeRuneInString(str[s:]) + if pr == sr { + p += psize + s += ssize + continue + } + } } - case '*': - return deepMatchRune(s, pattern[1:], simple) || - (len(s) > 0 && deepMatchRune(s[1:], pattern, simple)) } - s = s[1:] - pattern = pattern[1:] + + // backtrack if there was a previous '*' + if wildcardIdx != -1 { + p = wildcardIdx + 1 + matchIdx++ + s = matchIdx + continue + } + + // previous '*', and mismatch + return false + } + + // Skip remaining stars in pattern + for p < plen && pattern[p] == '*' { + p++ } - return len(s) == 0 && len(pattern) == 0 + + return p == plen } diff --git a/pkg/util/wildcard/wildcard_test.go b/pkg/util/wildcard/wildcard_test.go index 8168e64a0..19eb90f07 100644 --- a/pkg/util/wildcard/wildcard_test.go +++ b/pkg/util/wildcard/wildcard_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 by Nedim Sabic + * Copyright 2019-present by Nedim Sabic * http://rabbitstack.github.io * All Rights Reserved. * @@ -13,15 +13,31 @@ package wildcard import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestMatch(t *testing.T) { - assert.True(t, Match("C:\\*\\lsass?.dmp", "C:\\Windows\\System32\\lsass2.dmp")) - assert.True(t, Match("C:\\*\\ActionList.x?l", "C:\\Windows\\Setup\\LatentAcquisition\\ActionList.xml")) - assert.True(t, Match("C:\\ProgramData\\*.dll", "C:\\ProgramData\\Directory\\OneMoreDirectory\\mal.dll")) - assert.True(t, Match("C:\\ProgramData\\*.dll", "C:\\ProgramData\\Directory\\OneMoreDirectory\\mal.dll")) - assert.True(t, Match("HKEY_USERS\\*\\Environment\\windir", "HKEY_USERS\\S-1-5-21-2271034452-2606270099-984871569-1001\\Environment\\windir")) - assert.True(t, Match("C:\\Windows\\SoftwareDistribution\\*", "C:\\Windows\\SoftwareDistribution\\SLS\\7971F918-A847-4430-9279-4A52D1EFE18D\\sls.rar")) + var tests = []struct { + p string + s string + match bool + }{ + {"C:\\*\\lsass?.dmp", "C:\\Windows\\System32\\lsass2.dmp", true}, + {"?:\\*\\lsass?.dmp", "C:\\Windows\\System32\\lsass2.dmp", true}, + {"?:\\*\\lsass?.dmp", "C:\\Windows\\System32\\cmd.exe", false}, + {"C:\\*\\ActionList.x?l", "C:\\Windows\\Setup\\LatentAcquisition\\ActionList.xml", true}, + {"C:\\ProgramData\\*.dll", "C:\\ProgramData\\Directory\\OneMoreDirectory\\mal.dll", true}, + {"HKEY_USERS\\*\\Environment\\windir", "HKEY_USERS\\S-1-5-21-2271034452-2606270099-984871569-1001\\Environment\\windir", true}, + {"C:\\Windows\\SoftwareDistribution\\*", "C:\\Windows\\SoftwareDistribution\\SLS\\7971F918-A847-4430-9279-4A52D1EFE18D\\sls.rar", true}, + {"HKEY_USERS\\S-1-5-21-*_CLASSES\\MS-SETTINGS\\CURVER", "HKEY_USERS\\S-1-5-21-2271034452-1207270099-244871569-1021_CLASSES\\MS-SETTINGS\\CURVER", true}, + {"ntdll.dll|KernelBase.dll|advapi32.dll|*", "ntdll.dll|KernelBase.dll|advapi32.dll|pe386.dll|com.dll|clr.dll|mmc.exe", true}, + } + + for _, tt := range tests { + t.Run(tt.p, func(t *testing.T) { + assert.Equal(t, tt.match, Match(tt.p, tt.s)) + }) + } }