SNode.C
Loading...
Searching...
No Matches
regex_utils.cpp
Go to the documentation of this file.
1/*
2 * SNode.C - A Slim Toolkit for Network Communication
3 * Copyright (C) Volker Christian <me@vchrist.at>
4 * 2020, 2021, 2022, 2023, 2024, 2025
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published
8 * by the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20/*
21 * MIT License
22 *
23 * Permission is hereby granted, free of charge, to any person obtaining a copy
24 * of this software and associated documentation files (the "Software"), to deal
25 * in the Software without restriction, including without limitation the rights
26 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27 * copies of the Software, and to permit persons to whom the Software is
28 * furnished to do so, subject to the following conditions:
29 *
30 * The above copyright notice and this permission notice shall be included in
31 * all copies or substantial portions of the Software.
32 *
33 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
39 * THE SOFTWARE.
40 */
41
42#include "express/dispatcher/regex_utils.h"
43
44#include "express/Request.h"
45
46#ifndef DOXYGEN_SHOULD_SKIP_THIS
47
48#include <cstddef>
49
50#endif /* DOXYGEN_SHOULD_SKIP_THIS */
51
52namespace express::dispatcher {
53
54 std::vector<std::string> explode(const std::string& input, char delim) {
55 std::vector<std::string> result;
56 std::string current;
57 int parenDepth = 0;
58
59 for (const char ch : input) {
60 if (ch == '(') {
61 parenDepth++;
62 current += ch;
63 } else if (ch == ')') {
64 parenDepth--;
65 current += ch;
66 } else if (ch == delim && parenDepth == 0) {
67 result.push_back(current);
68 current.clear();
69 } else {
70 current += ch;
71 }
72 }
73
74 if (!current.empty()) {
75 result.push_back(current);
76 }
77
78 return result;
79 }
80
81#define PATH_REGEX ":[a-zA-Z0-9]+(\\‍(.+\\‍))?"
82 const std::regex& pathRegex() {
83 static const std::regex pathregex(PATH_REGEX);
84
85 return pathregex;
86 }
87
88 std::smatch matchResult(const std::string& cpath) {
89 std::smatch smatch;
90
91 std::regex_search(cpath, smatch, pathRegex());
92
93 return smatch;
94 }
95
96 bool hasResult(const std::string& cpath) {
97 std::smatch smatch;
98
99 return std::regex_search(cpath, smatch, pathRegex());
100 }
101
102 void setParams(const std::string& cpath, Request& req) {
103 std::vector<std::string> explodedString = explode(cpath, '/');
104 std::vector<std::string> explodedReqString = explode(req.url, '/');
105
106 for (std::vector<std::string>::size_type i = 0; i < explodedString.size() && i < explodedReqString.size(); i++) {
107 if (!explodedString[i].empty() && explodedString[i].front() == ':') {
108 const std::smatch smatch = matchResult(explodedString[i]);
109 std::string regex = "(.*)";
110
111 if (smatch.size() > 1) {
112 if (smatch[1] != "") {
113 regex = smatch[1];
114 }
115 }
116
117 if (std::regex_match(explodedReqString[i], std::regex(regex))) {
118 std::string attributeName = smatch[0];
119 attributeName.erase(0, 1);
120 attributeName.erase((attributeName.length() - static_cast<std::size_t>(smatch[1].length())),
121 static_cast<std::size_t>(smatch[1].length()));
122
123 req.params[attributeName] = explodedReqString[i];
124 }
125 }
126 }
127 }
128
129 std::unordered_map<std::string, std::string> parseQuery(std::string_view qs) {
130 std::unordered_map<std::string, std::string> m;
131 std::size_t i = 0;
132 while (i < qs.size()) {
133 const std::size_t amp = qs.find('&', i);
134 const std::string_view pair = (amp == std::string_view::npos) ? qs.substr(i) : qs.substr(i, amp - i);
135 const std::size_t eq = pair.find('=');
136 std::string key;
137 std::string val;
138 if (eq == std::string_view::npos) {
139 key.assign(pair);
140 } else {
141 key.assign(pair.substr(0, eq));
142 val.assign(pair.substr(eq + 1));
143 }
144 if (!key.empty()) {
145 m.emplace(std::move(key), std::move(val));
146 }
147 if (amp == std::string_view::npos) {
148 break;
149 }
150 i = amp + 1;
151 }
152 return m;
153 }
154
155 std::pair<std::regex, std::vector<std::string>>
156 compileParamRegex(std::string_view mountPath, bool isPrefix, bool strictRouting, bool caseInsensitive) {
157 std::string pat;
158 pat.reserve(mountPath.size() * 2);
159 std::vector<std::string> names;
160 pat += '^';
161
162 const char* s = mountPath.data();
163 const char* e = s + mountPath.size();
164 while (s < e) {
165 if (*s == ':') {
166 ++s;
167 const char* nstart = s;
168 while (s < e && (std::isalnum(static_cast<unsigned char>(*s)) > 0 || *s == '_' || *s == '-')) {
169 ++s;
170 }
171 std::string name(nstart, s);
172 std::string custom;
173 if (s < e && *s == '(') {
174 int depth = 0;
175 const char* rstart = s + 1;
176 ++s;
177 while (s < e) {
178 if (*s == '(') {
179 ++depth;
180 } else if (*s == ')' && depth-- == 0) {
181 break;
182 }
183 ++s;
184 }
185 custom.assign(rstart, s);
186 if (s < e && *s == ')') {
187 ++s;
188 }
189 }
190 names.push_back(std::move(name));
191 if (!custom.empty()) {
192 pat += '(';
193 pat += custom;
194 pat += ')';
195 } else {
196 pat += "([^/]+)";
197 } // default: single segment
198 } else {
199 static const std::string meta = R"(\.^$|()[]{}*+?!)";
200 if (meta.find(*s) != std::string::npos) {
201 pat += '\\';
202 }
203 pat += *s;
204 ++s;
205 }
206 }
207
208 if (isPrefix) {
209 pat += "(?:/|$)"; // boundary
210 } else {
211 if (!strictRouting) {
212 pat += "/?"; // allow single trailing slash
213 }
214 pat += '$';
215 }
216
217 auto flags = std::regex::ECMAScript | (!caseInsensitive ? std::regex::flag_type{} : std::regex::icase);
218
219 return {std::regex(pat, flags), std::move(names)};
220 }
221
222 bool querySupersetMatches(const std::unordered_map<std::string, std::string>& rq,
223 const std::unordered_map<std::string, std::string>& need) {
224 for (const auto& kv : need) {
225 auto it = rq.find(kv.first);
226 if (it == rq.end() || it->second != kv.second) {
227 return false;
228 }
229 }
230 return true;
231 }
232
233 bool boundaryPrefix(std::string_view path, std::string_view base, bool caseInsensitive) {
234 // Normalize: an empty base is equivalent to "/"
235 if (base.empty()) {
236 base = "/";
237 }
238
239 // Special case: base "/" matches any absolute path
240 if (base.size() == 1 && base[0] == '/') {
241 return !path.empty() && path.front() == '/';
242 }
243
244 // Base longer than path cannot match
245 if (base.size() > path.size()) {
246 return false;
247 }
248
249 auto eq = [&](char a, char b) {
250 return !caseInsensitive ? (a == b)
251 : (std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)));
252 };
253
254 // Check prefix characters
255 for (size_t i = 0; i < base.size(); ++i) {
256 if (!eq(path[i], base[i])) {
257 return false;
258 }
259 }
260
261 // Boundary: either exact match, or next char is a '/'
262 return (path.size() == base.size()) || (path[base.size()] == '/');
263 }
264
265 bool equalPath(std::string_view a, std::string_view b, bool caseInsensitive) {
266 if (a.size() != b.size()) {
267 return false;
268 }
269 for (size_t i = 0; i < a.size(); ++i) {
270 if (!caseInsensitive ? (a[i] != b[i]) : !ieq(a[i], b[i])) {
271 return false;
272 }
273 }
274 return true;
275 }
276
277 std::string_view trimOneTrailingSlash(std::string_view s) {
278 if (s.size() > 1 && s.back() == '/') {
279 return std::string_view(s.data(), s.size() - 1);
280 }
281 return s;
282 }
283
284 void splitPathAndQuery(std::string_view url, std::string_view& path, std::string_view& query) {
285 const std::size_t qpos = url.find('?');
286 if (qpos == std::string_view::npos) {
287 path = url;
288 query = {};
289 } else {
290 path = url.substr(0, qpos);
291 query = url.substr(qpos + 1);
292 }
293 }
294
295} // namespace express::dispatcher
std::string url
Definition Request.h:96
std::map< std::string, std::string > params
Definition Request.h:79
bool querySupersetMatches(const std::unordered_map< std::string, std::string > &rq, const std::unordered_map< std::string, std::string > &need)
void setParams(const std::string &cpath, Request &req)
std::vector< std::string > explode(const std::string &input, char delim)
void splitPathAndQuery(std::string_view url, std::string_view &path, std::string_view &query)
bool boundaryPrefix(std::string_view path, std::string_view base, bool caseInsensitive)
std::pair< std::regex, std::vector< std::string > > compileParamRegex(std::string_view mountPath, bool isPrefix, bool strictRouting, bool caseInsensitive)
std::smatch matchResult(const std::string &cpath)
bool ieq(char a, char b)
Definition regex_utils.h:84
std::unordered_map< std::string, std::string > parseQuery(std::string_view qs)
bool hasResult(const std::string &cpath)
std::string_view trimOneTrailingSlash(std::string_view s)
bool equalPath(std::string_view a, std::string_view b, bool caseInsensitive)
const std::regex & pathRegex()
#define PATH_REGEX