The FreeRADIUS server $Id: 15bac2a4c627c01d1aa2047687b3418955ac7f00 $
Loading...
Searching...
No Matches
skip.c
Go to the documentation of this file.
1/*
2 * This library is free software; you can redistribute it and/or
3 * modify it under the terms of the GNU Lesser General Public
4 * License as published by the Free Software Foundation; either
5 * version 2.1 of the License, or (at your option) any later version.
6 *
7 * This library is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 * Lesser General Public License for more details.
11 *
12 * You should have received a copy of the GNU Lesser General Public
13 * License along with this library; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17/** Preparse input by skipping known tokens
18 *
19 * @file src/lib/util/skip.c
20 *
21 * @copyright 2025 Network RADIUS SAS (legal@networkradius.com)
22 */
23RCSID("$Id: c6102ea34d90bbe0ef8f3ea6118a402f3ceb6546 $")
24
25#include <freeradius-devel/util/misc.h>
26#include <freeradius-devel/util/skip.h>
27#include <freeradius-devel/util/strerror.h>
28
29/** Skip a quoted string.
30 *
31 * @param[in] start start of the string, pointing to the quotation character
32 * @param[in] end end of the string (or NULL for zero-terminated strings)
33 * @return
34 * >0 length of the string which was parsed
35 * <=0 on error
36 */
37ssize_t fr_skip_string(char const *start, char const *end)
38{
39 char const *p = start;
40 char quote;
41
42 quote = *(p++);
43
44 while ((end && (p < end)) || *p) {
45 /*
46 * Stop at the quotation character
47 */
48 if (*p == quote) {
49 p++;
50 return p - start;
51 }
52
53 /*
54 * Not an escape character: it's OK.
55 */
56 if (*p != '\\') {
57 p++;
58 continue;
59 }
60
61 if (end && ((p + 2) >= end)) {
62 fail:
63 fr_strerror_const("Unexpected escape at end of string");
64 return -(p - start);
65 }
66
67 /*
68 * Escape at EOL is not allowed.
69 */
70 if (p[1] < ' ') goto fail;
71
72 /*
73 * \r or \n, etc.
74 */
75 if (!isdigit((uint8_t) p[1])) {
76 p += 2;
77 continue;
78 }
79
80 /*
81 * Double-quoted strings use \000
82 * Regexes use \0
83 */
84 if (quote == '/') {
85 p++;
86 continue;
87 }
88
89 if (end && ((p + 4) >= end)) goto fail;
90
91 /*
92 * Allow for \1f in single quoted strings
93 */
94 if ((quote == '\'') && isxdigit((uint8_t) p[1]) && isxdigit((uint8_t) p[2])) {
95 p += 3;
96 continue;
97 }
98
99 if (!isdigit((uint8_t) p[2]) || !isdigit((uint8_t) p[3])) {
100 fr_strerror_const("Invalid octal escape");
101 return -(p - start);
102 }
103
104 p += 4;
105 }
106
107 /*
108 * Unexpected end of string.
109 */
110 fr_strerror_const("Unexpected end of string");
111 return -(p - start);
112}
113
114/** Skip a generic {...} or (...) arguments
115 *
116 */
117ssize_t fr_skip_brackets(char const *start, char const *end, char end_quote)
118{
119 ssize_t slen;
120 char const *p = start;
121
122 while ((end && (p < end)) || *p) {
123 if (*p == end_quote) {
124 p++;
125 return p - start;
126 }
127
128 /*
129 * Expressions. Arguably we want to
130 * differentiate conditions and function
131 * arguments, but it's not clear how to do that
132 * in a pre-parsing stage.
133 */
134 if (*p == '(') {
135 p++;
136 slen = fr_skip_brackets(p, end, ')');
137 if (slen <= 0) return slen - (p - start);
138
139 next:
140 fr_assert((size_t) slen <= (size_t) (end - p));
141 p += slen;
142 continue;
143 }
144
145 /*
146 * A quoted string.
147 */
148 if ((*p == '"') || (*p == '\'') || (*p == '`')) {
149 slen = fr_skip_string(p, end);
150 goto next;
151 }
152
153 /*
154 * Nested expansion.
155 */
156 if ((p[0] == '$') || (p[0] == '%')) {
157 if (end && (p + 2) >= end) break;
158
159 /*
160 * %% inside of an xlat
161 */
162 if ((p[0] == '%') && (p[1] == '%')) {
163 p += 2;
164 continue;
165 }
166
167 if ((p[1] == '{') || (p[1] == '(')) {
168 slen = fr_skip_xlat(p, end);
169 goto next;
170 }
171
172 /*
173 * Bare $ or %, just leave it alone.
174 */
175 p++;
176 continue;
177 }
178
179 /*
180 * Escapes are special.
181 */
182 if (*p != '\\') {
183 p++;
184 continue;
185 }
186
187 if (end && ((p + 2) >= end)) break;
188
189 /*
190 * Escapes here are only one-character escapes.
191 */
192 if (p[1] < ' ') break;
193 p += 2;
194 }
195
196 /*
197 * Unexpected end of xlat
198 */
199 fr_strerror_const("Unexpected end of expansion");
200 return -(p - start);
201}
202
203/** Skip an xlat expression.
204 *
205 * This is a simple "peek ahead" parser which tries to not be wrong. It may accept
206 * some things which will later parse as invalid (e.g. unknown attributes, etc.)
207 * But it also rejects all malformed expressions.
208 *
209 * It's used as a quick hack because the full parser isn't always available.
210 *
211 * @param[in] start start of the expression, MUST point to the "%{" or "%("
212 * @param[in] end end of the string (or NULL for zero-terminated strings)
213 * @return
214 * >0 length of the string which was parsed
215 * <=0 on error
216 */
217ssize_t fr_skip_xlat(char const *start, char const *end)
218{
219 ssize_t slen;
220 char const *p = start;
221
222 /*
223 * At least %{1} or $(.)
224 */
225 if (end && ((end - start) < 4)) {
226 fr_strerror_const("Invalid expansion");
227 return 0;
228 }
229
230 if (!((memcmp(p, "%{", 2) == 0) || /* xlat */
231 (memcmp(p, "${", 2) == 0) || /* config file macro */
232 (memcmp(p, "$(", 2) == 0))) { /* shell expansion in an back-ticks argument */
233 fr_strerror_const("Invalid expansion");
234 return 0;
235 }
236 p++;
237
238 if (*p == '(') {
239 p++; /* skip the '(' */
240 slen = fr_skip_brackets(p, end, ')');
241
242 } else if (*p == '{') {
243 p++; /* skip the '{' */
244 slen = fr_skip_brackets(p, end, '}');
245
246 } else {
247 char const *q = p;
248
249 /*
250 * New xlat syntax: %foo(...)
251 */
252 while (isalnum((int) *q) || (*q == '.') || (*q == '_') || (*q == '-')) {
253 q++;
254 }
255
256 if (*q != '(') {
257 fr_strerror_const("Invalid character after '%'");
258 return -(p - start);
259 }
260
261 p = q + 1;
262
263 slen = fr_skip_brackets(p, end, ')');
264 }
265
266 if (slen <= 0) return slen - (p - start);
267 return slen + (p - start);
268}
269
270/** Skip a conditional expression.
271 *
272 * This is a simple "peek ahead" parser which tries to not be wrong. It may accept
273 * some things which will later parse as invalid (e.g. unknown attributes, etc.)
274 * But it also rejects all malformed expressions.
275 *
276 * It's used as a quick hack because the full parser isn't always available.
277 *
278 * @param[in] start start of the condition.
279 * @param[in] end end of the string (or NULL for zero-terminated strings)
280 * @param[in] terminal terminal character(s)
281 * @param[out] eol did the parse error happen at eol?
282 * @return
283 * >0 length of the string which was parsed. *eol is false.
284 * <=0 on error, *eol may be set.
285 */
286ssize_t fr_skip_condition(char const *start, char const *end, bool const terminal[static UINT8_MAX + 1], bool *eol)
287{
288 char const *p = start;
289 bool was_regex = false;
290 int depth = 0;
291 ssize_t slen;
292
293 if (eol) *eol = false;
294
295 /*
296 * Keep parsing the condition until we hit EOS or EOL.
297 */
298 while ((end && (p < end)) || *p) {
299 if (isspace((uint8_t) *p)) {
300 p++;
301 continue;
302 }
303
304 /*
305 * In the configuration files, conditions end with ") {" or just "{"
306 */
307 if ((depth == 0) && terminal[(uint8_t) *p]) {
308 return p - start;
309 }
310
311 /*
312 * "recurse" to get more conditions.
313 */
314 if (*p == '(') {
315 p++;
316 depth++;
317 was_regex = false;
318 continue;
319 }
320
321 if (*p == ')') {
322 if (!depth) {
323 fr_strerror_const("Too many ')'");
324 return -(p - start);
325 }
326
327 p++;
328 depth--;
329 was_regex = false;
330 continue;
331 }
332
333 /*
334 * Parse xlats. They cannot span EOL.
335 */
336 if ((*p == '$') || (*p == '%')) {
337 if (end && ((p + 2) >= end)) {
338 fr_strerror_const("Expansions cannot extend across end of line");
339 return -(p - start);
340 }
341
342 if ((p[1] == '{') || ((p[0] == '$') && (p[1] == '('))) {
343 slen = fr_skip_xlat(p, end);
344
345 check:
346 if (slen <= 0) return -(p - start) + slen;
347
348 p += slen;
349 continue;
350 }
351
352 /*
353 * Bare $ or %, just leave it alone.
354 */
355 p++;
356 was_regex = false;
357 continue;
358 }
359
360 /*
361 * Parse quoted strings. They cannot span EOL.
362 */
363 if ((*p == '"') || (*p == '\'') || (*p == '`') || (was_regex && (*p == '/'))) {
364 was_regex = false;
365
366 slen = fr_skip_string((char const *) p, end);
367 goto check;
368 }
369
370 /*
371 * 192.168/16 is a netmask. So we only
372 * allow regex after a regex operator.
373 *
374 * This isn't perfect, but is good enough
375 * for most purposes.
376 */
377 if ((p[0] == '=') || (p[0] == '!')) {
378 if (end && ((p + 2) >= end)) {
379 fr_strerror_const("Operators cannot extend across end of line");
380 return -(p - start);
381 }
382
383 if (p[1] == '~') {
384 was_regex = true;
385 p += 2;
386 continue;
387 }
388
389 /*
390 * Some other '==' or '!=', just leave it alone.
391 */
392 p++;
393 was_regex = false;
394 continue;
395 }
396
397 /*
398 * Any control characters (other than \t) cause an error.
399 */
400 if (*p < ' ') break;
401
402 was_regex = false;
403
404 /*
405 * Normal characters just get skipped.
406 */
407 if (*p != '\\') {
408 p++;
409 continue;
410 }
411
412 /*
413 * Backslashes at EOL are ignored.
414 */
415 if (end && ((p + 2) >= end)) break;
416
417 /*
418 * Escapes here are only one-character escapes.
419 */
420 if (p[1] < ' ') break;
421 p += 2;
422 }
423
424 /*
425 * We've fallen off of the end of a string. It may be OK?
426 */
427 if (eol) *eol = (depth > 0);
428
429 if (terminal[(uint8_t) *p]) return p - start;
430
431 fr_strerror_const("Unexpected end of condition");
432 return -(p - start);
433}
434
#define RCSID(id)
Definition build.h:485
long int ssize_t
unsigned char uint8_t
#define UINT8_MAX
static uint8_t depth(fr_minmax_heap_index_t i)
Definition minmax_heap.c:83
#define fr_assert(_expr)
Definition rad_assert.h:38
ssize_t fr_skip_condition(char const *start, char const *end, bool const terminal[static UINT8_MAX+1], bool *eol)
Skip a conditional expression.
Definition skip.c:286
ssize_t fr_skip_brackets(char const *start, char const *end, char end_quote)
Skip a generic {...} or (...) arguments.
Definition skip.c:117
ssize_t fr_skip_string(char const *start, char const *end)
Skip a quoted string.
Definition skip.c:37
ssize_t fr_skip_xlat(char const *start, char const *end)
Skip an xlat expression.
Definition skip.c:217
#define fr_strerror_const(_msg)
Definition strerror.h:223