Skip to content

Commit 2e99457

Browse files
committed
feat: skip regex for plain paths and cache compiled regexes in keyMatch functions
1 parent 78b5968 commit 2e99457

2 files changed

Lines changed: 61 additions & 5 deletions

File tree

‎casbin/util/built_in_functions.cpp‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include <map>
2323
#include <regex>
24+
#include <unordered_map>
2425

2526
#include "casbin/exception/illegal_argument_exception.h"
2627
#include "casbin/ip_parser/parser/CIDR.h"
@@ -57,6 +58,40 @@ namespace {
5758
std::string intermediate = ReplacePattern(value, "{", "\\{");
5859
return ReplacePattern(intermediate, "}", "\\}");
5960
}
61+
62+
static const char regexSpecialChars[] = "\\^$.|?*+()[]{}";
63+
static const char regexSpecialCharsAndColon[] = "\\^$.|?*+()[]{}:";
64+
65+
// MatchWithoutRegex matches key1 against key2 without a regex when key2 is a plain path, optionally ending with "/*".
66+
// It returns false when key2 contains other special characters, then the caller has to fall back to the regex.
67+
bool MatchWithoutRegex(const std::string& key1, const std::string& key2, const char* special_chars, bool& res) {
68+
std::size_t pos = key2.find_first_of(special_chars);
69+
if (pos == std::string::npos) {
70+
res = key1 == key2;
71+
return true;
72+
}
73+
74+
if (pos > 0 && pos + 1 == key2.size() && key2[pos] == '*' && key2[pos - 1] == '/') {
75+
// Same as "/.*" in the regex: same prefix, and "." does not match line terminators.
76+
res = key1.compare(0, pos, key2, 0, pos) == 0 && key1.find_first_of("\r\n", pos) == std::string::npos;
77+
return true;
78+
}
79+
80+
return false;
81+
}
82+
83+
// GetRegex returns the compiled regex of pattern, each thread compiles a pattern only once.
84+
const std::regex& GetRegex(const std::string& pattern) {
85+
thread_local std::unordered_map<std::string, std::regex> cache;
86+
auto it = cache.find(pattern);
87+
if (it != cache.end())
88+
return it->second;
89+
90+
if (cache.size() >= 1024)
91+
cache.clear();
92+
93+
return cache.emplace(pattern, std::regex(pattern)).first->second;
94+
}
6095
}
6196

6297
// KeyMatch determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
@@ -92,6 +127,10 @@ std::string KeyGet(const std::string& key1, const std::string& key2) {
92127
// KeyMatch2 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
93128
// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource"
94129
bool KeyMatch2(const std::string& key1, const std::string& key2) {
130+
bool res;
131+
if (MatchWithoutRegex(key1, key2, regexSpecialCharsAndColon, res))
132+
return res;
133+
95134
std::string k2 = PrepareWildCardMatching(key2);
96135
k2 = std::regex_replace(k2, capturingColonNonSlashRegex, "$1[^/]+$2");
97136
k2 = EscapeCurlyBraces(k2);
@@ -121,7 +160,7 @@ std::string KeyGet2(const std::string& key1, const std::string& key2, const std:
121160
k2 = "^" + k2 + "$";
122161

123162
std::smatch values;
124-
std::regex_match(key1.begin(), key1.end(), values, std::regex(k2));
163+
std::regex_match(key1.begin(), key1.end(), values, GetRegex(k2));
125164

126165
for (int i = 0; i < keys.size(); i++)
127166
if (!path_var.compare(keys.at(i).substr(1)))
@@ -133,6 +172,10 @@ std::string KeyGet2(const std::string& key1, const std::string& key2, const std:
133172
// KeyMatch3 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
134173
// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/{resource}"
135174
bool KeyMatch3(const std::string& key1, const std::string& key2) {
175+
bool res;
176+
if (MatchWithoutRegex(key1, key2, regexSpecialChars, res))
177+
return res;
178+
136179
std::string k2 = PrepareWildCardMatching(key2);
137180
k2 = std::regex_replace(k2, enclosedPlaceHolderRegex, "$1[^/]+$2");
138181
k2 = EscapeCurlyBraces(k2);
@@ -159,7 +202,7 @@ std::string KeyGet3(const std::string& key1, const std::string& key2, const std:
159202
k2 = "^" + k2 + "$";
160203

161204
std::smatch values;
162-
std::regex_match(key1.begin(), key1.end(), values, std::regex(k2));
205+
std::regex_match(key1.begin(), key1.end(), values, GetRegex(k2));
163206

164207
for (int i = 0; i < keys.size(); i++)
165208
if (!path_var.compare(keys.at(i).substr(1, keys.at(i).length() - 2)))
@@ -174,6 +217,10 @@ std::string KeyGet3(const std::string& key1, const std::string& key2, const std:
174217
// "/parent/123/child/456" does not match "/parent/{id}/child/{id}"
175218
// But KeyMatch3 will match both.
176219
bool KeyMatch4(const std::string& key1, const std::string& key2) {
220+
bool res;
221+
if (MatchWithoutRegex(key1, key2, regexSpecialChars, res))
222+
return res;
223+
177224
std::string k2 = PrepareWildCardMatching(key2);
178225

179226
std::vector<std::string> tokens;
@@ -185,7 +232,7 @@ bool KeyMatch4(const std::string& key1, const std::string& key2) {
185232
k2 = EscapeCurlyBraces(k2);
186233
k2 = "^" + k2 + "$";
187234
std::smatch matches;
188-
std::regex_match(key1.begin(), key1.end(), matches, std::regex(k2));
235+
std::regex_match(key1.begin(), key1.end(), matches, GetRegex(k2));
189236
if (matches.empty())
190237
return false;
191238
if (tokens.size() != matches.size() - 1)
@@ -213,8 +260,7 @@ bool KeyMatch5(const std::string& key1, const std::string& key2) {
213260

214261
// RegexMatch determines whether key1 matches the pattern of key2 in regular expression.
215262
bool RegexMatch(const std::string& key1, const std::string& key2) {
216-
std::regex regex_s(key2);
217-
return std::regex_match(key1, regex_s);
263+
return std::regex_match(key1, GetRegex(key2));
218264
}
219265

220266
// IPMatch determines whether IP address ip1 matches the pattern of IP address ip2, ip2 can be an IP address or a CIDR pattern.

‎tests/built_in_functions_test.cpp‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ TEST(TestBuiltInFunctions, TestKeyMatch2) {
9393
TestKeyMatch2Fn("/foobar", "/foo", false);
9494
TestKeyMatch2Fn("/foobar", "/foo*", false); // different with KeyMatch.
9595
TestKeyMatch2Fn("/foobar", "/foo/*", false);
96+
TestKeyMatch2Fn("/foo/", "/foo/*", true);
97+
TestKeyMatch2Fn("/foo/bar\nbaz", "/foo/*", false);
98+
TestKeyMatch2Fn("/foo.bar", "/foo.bar", true);
9699

97100
TestKeyMatch2Fn("/", "/:resource", false);
98101
TestKeyMatch2Fn("/resource1", "/:resource", true);
@@ -186,6 +189,9 @@ TEST(TestBuiltInFunctions, TestKeyMatch3) {
186189
TestKeyMatch3Fn("/foobar", "/foo", false);
187190
TestKeyMatch3Fn("/foobar", "/foo*", false);
188191
TestKeyMatch3Fn("/foobar", "/foo/*", false);
192+
TestKeyMatch3Fn("/foo/", "/foo/*", true);
193+
TestKeyMatch3Fn("/foo/bar\nbaz", "/foo/*", false);
194+
TestKeyMatch3Fn("/foo:bar", "/foo:bar", true);
189195

190196
TestKeyMatch3Fn("/", "/{resource}", false);
191197
TestKeyMatch3Fn("/resource1", "/{resource}", true);
@@ -280,6 +286,10 @@ TEST(TestBuiltInFunctions, TestKeyMatch4) {
280286
TestKeyMatch4Fn("/parent/123/child/456", "/parent/{id}/child/{id}/book/{id}", false);
281287

282288
TestKeyMatch4Fn("/parent/123/child/123", "/parent/{i/d}/child/{i/d}", false);
289+
290+
TestKeyMatch4Fn("/parent/123", "/parent/123", true);
291+
TestKeyMatch4Fn("/parent/123", "/parent/*", true);
292+
TestKeyMatch4Fn("/parent", "/parent/*", false);
283293
}
284294

285295
void TestKeyMatch5Fn(std::string key1, std::string key2, bool res) {

0 commit comments

Comments
 (0)