ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
regex.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file regex.cpp
6 * @brief Bounded regex route matcher for PC (used by on_regex() routes).
7 *
8 * Split out of protocore.cpp (single-purpose server files). A small recursive backtracker over
9 * one pattern - literals, '.', quantifiers, character classes, and escapes - with a step budget
10 * (RE_MAX_STEPS) so a pathological pattern fails closed instead of backtracking unboundedly,
11 * preserving determinism. No heap, no groups, no alternation. The route dispatcher calls
12 * regex_match() (declared in server/protocore_internal.h). Behavior is identical to the pre-split code.
13 */
14
15#include "protocore.h" // RE_MAX_STEPS (ServerConfig), fixed-width types
16#include "server/protocore_internal.h" // regex_match declaration
17#include <string.h>
18
19// ---------------------------------------------------------------------------
20// Bounded regex route matcher (see on_regex()).
21//
22// A small recursive backtracker over a single pattern (no heap, no groups, no
23// alternation). Supported: literals, '.', quantifiers '*' '+' '?', character
24// classes [..]/[^..] with a-z ranges, and '\' escapes incl. \d \w \s (\D \W \S).
25// A step counter bounds total work so a pathological pattern fails closed
26// (no match) instead of backtracking unboundedly - preserving determinism.
27// ---------------------------------------------------------------------------
28
29struct ReCtx
30{
31 uint32_t steps;
32 uint32_t max_steps;
33};
34
35// Byte length of the atom at p: an escape (\x), a class ([...]), or one char.
36static size_t re_atom_len(const char *p)
37{
38 if (*p == '\\')
39 {
40 return p[1] ? 2 : 1;
41 }
42 if (*p == '[')
43 {
44 const char *q = p + 1;
45 if (*q == '^')
46 {
47 q++;
48 }
49 if (*q == ']') // a ']' right after '[' (or '[^') is a literal member
50 {
51 q++;
52 }
53 while (*q && *q != ']')
54 {
55 if (*q == '\\' && q[1])
56 {
57 q += 2;
58 }
59 else
60 {
61 q++;
62 }
63 }
64 return (size_t)((*q == ']' ? q + 1 : q) - p);
65 }
66 return 1;
67}
68
69static bool re_class_member(char lo, char hi, char ch)
70{
71 return ch >= lo && ch <= hi;
72}
73
74// Read one class atom at *q (a backslash-escape consumes 2 bytes, else 1), advancing q past it.
75static char re_read_atom(const char *&q, const char *end)
76{
77 if (*q == '\\' && (q + 1) < end)
78 {
79 char c = q[1];
80 q += 2;
81 return c;
82 }
83 char c = *q;
84 q++;
85 return c;
86}
87
88// Match a backslash-escape class (\d \D \w \W \s \S) or an escaped literal against ch.
89static bool re_match_escape(char e, char ch)
90{
91 switch (e)
92 {
93 case 'd':
94 return ch >= '0' && ch <= '9';
95 case 'D':
96 return !(ch >= '0' && ch <= '9');
97 case 'w':
98 return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_';
99 case 'W':
100 return !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_');
101 case 's':
102 return ch == ' ' || ch == '\t';
103 case 'S':
104 return !(ch == ' ' || ch == '\t');
105 default:
106 return ch == e; // escaped literal (\. \* \\ ...)
107 }
108}
109
110// Match a '[...]' character class (optional '^' negation, a-z ranges) at [p, p+len) against ch.
111static bool re_match_class(const char *p, size_t len, char ch)
112{
113 const char *q = p + 1;
114 const char *end = p + len - 1; // points at the closing ']'
115 bool neg = false;
116 if (q < end && *q == '^')
117 {
118 neg = true;
119 q++;
120 }
121 bool m = false;
122 while (q < end)
123 {
124 char lo = re_read_atom(q, end);
125 // The q[1] != ']' arm below can never be false, so this line is branch-excluded. re_atom_len
126 // ends the class at the FIRST unescaped ']', so if q[1] is a ']' it IS the terminator - then
127 // q + 1 == end and the preceding (q + 1) < end has already short-circuited. Confirmed by
128 // exhaustive search over every class body of length <= 7 drawn from { [ ] ^ - \ a }.
129 if (q < end && *q == '-' && (q + 1) < end && q[1] != ']') // GCOVR_EXCL_LINE see note above
130 {
131 q++; // consume '-'
132 char hi = re_read_atom(q, end);
133 m = re_class_member(lo, hi, ch) || m;
134 }
135 else if (ch == lo)
136 {
137 m = true;
138 }
139 }
140 return neg ? !m : m;
141}
142
143// Does the atom [p, p+len) match the single character ch (ch != '\0')?
144static bool re_atom_matches(const char *p, size_t len, char ch)
145{
146 if (ch == '\0')
147 {
148 return false;
149 }
150 if (*p == '\\')
151 {
152 return re_match_escape(p[1], ch);
153 }
154 if (*p == '.')
155 {
156 return true;
157 }
158 if (*p == '[')
159 {
160 return re_match_class(p, len, ch);
161 }
162 return ch == *p; // literal
163}
164
165static bool re_match(ReCtx *c, const char *pat, const char *text);
166
167// Greedy "(atom)* rest" against text.
168static bool re_star(ReCtx *c, const char *atom, size_t al, const char *rest, const char *text)
169{
170 if (++c->steps > c->max_steps)
171 {
172 return false;
173 }
174 if (re_atom_matches(atom, al, *text) && re_star(c, atom, al, rest, text + 1))
175 {
176 return true;
177 }
178 return re_match(c, rest, text);
179}
180
181static bool re_match(ReCtx *c, const char *pat, const char *text)
182{
183 if (++c->steps > c->max_steps)
184 {
185 return false;
186 }
187 if (*pat == '\0')
188 {
189 return *text == '\0'; // full-match: pattern and text end together
190 }
191
192 size_t al = re_atom_len(pat);
193 char quant = pat[al];
194 const char *rest = (quant == '*' || quant == '+' || quant == '?') ? pat + al + 1 : pat + al;
195
196 if (quant == '*')
197 {
198 return re_star(c, pat, al, rest, text);
199 }
200 if (quant == '+')
201 {
202 if (!re_atom_matches(pat, al, *text))
203 {
204 return false;
205 }
206 return re_star(c, pat, al, rest, text + 1);
207 }
208 if (quant == '?')
209 {
210 if (re_atom_matches(pat, al, *text) && re_match(c, rest, text + 1))
211 {
212 return true;
213 }
214 return re_match(c, rest, text);
215 }
216 // exactly one
217 if (re_atom_matches(pat, al, *text))
218 {
219 return re_match(c, rest, text + 1);
220 }
221 return false;
222}
223
224// Whole-path regex match (implicitly anchored at both ends). External linkage
225// (declared in server/protocore_internal.h): the route dispatcher calls it.
226bool regex_match(const char *pattern, const char *path)
227{
228 ReCtx c;
229 c.steps = 0;
231 return re_match(&c, pattern, path);
232}
Layer 7 (Application) - public HTTP routing API.
#define RE_MAX_STEPS
Step budget for the regex route matcher (see on_regex()).
Library-private declarations shared between protocore.cpp and the src/server/*.cpp request-handler tr...
bool regex_match(const char *pattern, const char *path)
Whole-path regex match (anchored both ends; bounded by RE_MAX_STEPS, fails closed)....
Definition regex.cpp:226
uint32_t max_steps
Definition regex.cpp:32
uint32_t steps
Definition regex.cpp:31