DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DetWebServer (used by on_regex() routes).
7 *
8 * Split out of dwserver.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/dwserver_internal.h). Behaviour is identical to the pre-split code.
13 */
14
15#include "dwserver.h" // RE_MAX_STEPS (ServerConfig), fixed-width types
16#include "server/dwserver_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 return p[1] ? 2 : 1;
40 if (*p == '[')
41 {
42 const char *q = p + 1;
43 if (*q == '^')
44 q++;
45 if (*q == ']') // a ']' right after '[' (or '[^') is a literal member
46 q++;
47 while (*q && *q != ']')
48 {
49 if (*q == '\\' && q[1])
50 q += 2;
51 else
52 q++;
53 }
54 return (size_t)((*q == ']' ? q + 1 : q) - p);
55 }
56 return 1;
57}
58
59static bool re_class_member(char lo, char hi, char ch)
60{
61 return ch >= lo && ch <= hi;
62}
63
64// Read one class atom at *q (a backslash-escape consumes 2 bytes, else 1), advancing q past it.
65static char re_read_atom(const char *&q, const char *end)
66{
67 if (*q == '\\' && (q + 1) < end)
68 {
69 char c = q[1];
70 q += 2;
71 return c;
72 }
73 char c = *q;
74 q++;
75 return c;
76}
77
78// Match a backslash-escape class (\d \D \w \W \s \S) or an escaped literal against ch.
79static bool re_match_escape(char e, char ch)
80{
81 switch (e)
82 {
83 case 'd':
84 return ch >= '0' && ch <= '9';
85 case 'D':
86 return !(ch >= '0' && ch <= '9');
87 case 'w':
88 return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_';
89 case 'W':
90 return !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_');
91 case 's':
92 return ch == ' ' || ch == '\t';
93 case 'S':
94 return !(ch == ' ' || ch == '\t');
95 default:
96 return ch == e; // escaped literal (\. \* \\ ...)
97 }
98}
99
100// Match a '[...]' character class (optional '^' negation, a-z ranges) at [p, p+len) against ch.
101static bool re_match_class(const char *p, size_t len, char ch)
102{
103 const char *q = p + 1;
104 const char *end = p + len - 1; // points at the closing ']'
105 bool neg = false;
106 if (q < end && *q == '^')
107 {
108 neg = true;
109 q++;
110 }
111 bool m = false;
112 while (q < end)
113 {
114 char lo = re_read_atom(q, end);
115 if (q < end && *q == '-' && (q + 1) < end && q[1] != ']')
116 {
117 q++; // consume '-'
118 char hi = re_read_atom(q, end);
119 m = re_class_member(lo, hi, ch) || m;
120 }
121 else if (ch == lo)
122 {
123 m = true;
124 }
125 }
126 return neg ? !m : m;
127}
128
129// Does the atom [p, p+len) match the single character ch (ch != '\0')?
130static bool re_atom_matches(const char *p, size_t len, char ch)
131{
132 if (ch == '\0')
133 return false;
134 if (*p == '\\')
135 return re_match_escape(p[1], ch);
136 if (*p == '.')
137 return true;
138 if (*p == '[')
139 return re_match_class(p, len, ch);
140 return ch == *p; // literal
141}
142
143static bool re_match(ReCtx *c, const char *pat, const char *text);
144
145// Greedy "(atom)* rest" against text.
146static bool re_star(ReCtx *c, const char *atom, size_t al, const char *rest, const char *text)
147{
148 if (++c->steps > c->max_steps)
149 return false;
150 if (re_atom_matches(atom, al, *text) && re_star(c, atom, al, rest, text + 1))
151 return true;
152 return re_match(c, rest, text);
153}
154
155static bool re_match(ReCtx *c, const char *pat, const char *text)
156{
157 if (++c->steps > c->max_steps)
158 return false;
159 if (*pat == '\0')
160 return *text == '\0'; // full-match: pattern and text end together
161
162 size_t al = re_atom_len(pat);
163 char quant = pat[al];
164 const char *rest = (quant == '*' || quant == '+' || quant == '?') ? pat + al + 1 : pat + al;
165
166 if (quant == '*')
167 return re_star(c, pat, al, rest, text);
168 if (quant == '+')
169 {
170 if (!re_atom_matches(pat, al, *text))
171 return false;
172 return re_star(c, pat, al, rest, text + 1);
173 }
174 if (quant == '?')
175 {
176 if (re_atom_matches(pat, al, *text) && re_match(c, rest, text + 1))
177 return true;
178 return re_match(c, rest, text);
179 }
180 // exactly one
181 if (re_atom_matches(pat, al, *text))
182 return re_match(c, rest, text + 1);
183 return false;
184}
185
186// Whole-path regex match (implicitly anchored at both ends). External linkage
187// (declared in server/dwserver_internal.h): the route dispatcher calls it.
188bool regex_match(const char *pattern, const char *path)
189{
190 ReCtx c;
191 c.steps = 0;
193 return re_match(&c, pattern, path);
194}
#define RE_MAX_STEPS
Step budget for the regex route matcher (see on_regex()).
Layer 7 (Application) - public HTTP routing API.
Library-private declarations shared between dwserver.cpp and the src/server/*.cpp request-handler tra...
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:188
uint32_t max_steps
Definition regex.cpp:32
uint32_t steps
Definition regex.cpp:31