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