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: 81d7129491616e55b92ec83ec642e20be1675c23 $")
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 an xlat expression.
115 *
116 * This is a simple "peek ahead" parser which tries to not be wrong. It may accept
117 * some things which will later parse as invalid (e.g. unknown attributes, etc.)
118 * But it also rejects all malformed expressions.
119 *
120 * It's used as a quick hack because the full parser isn't always available.
121 *
122 * @param[in] start start of the expression, MUST point to the "%{" or "%("
123 * @param[in] end end of the string (or NULL for zero-terminated strings)
124 * @return
125 * >0 length of the string which was parsed
126 * <=0 on error
127 */
128ssize_t fr_skip_xlat(char const *start, char const *end)
129{
130 int depth = 1; /* caller skips '{' */
131 ssize_t slen;
132 char quote, end_quote;
133 char const *p = start;
134
135 /*
136 * At least %{1}
137 */
138 if (end && ((start + 4) > end)) {
139 fr_strerror_const("Invalid expansion");
140 return 0;
141 }
142
143 if ((*p != '%') && (*p != '$')) {
144 fr_strerror_const("Unexpected character in expansion");
145 return -(p - start);
146 }
147
148 p++;
149 if ((*p != '{') && (*p != '(')) {
150 char const *q = p;
151
152 /*
153 * New xlat syntax: %foo(...)
154 */
155 while (isalnum((int) *q) || (*q == '.') || (*q == '_') || (*q == '-')) {
156 q++;
157 }
158 if (*q == '(') {
159 p = q;
160 goto do_quote;
161 }
162
163 fr_strerror_const("Invalid character after '%'");
164 return -(p - start);
165 }
166
167do_quote:
168 quote = *(p++);
169 if (quote == '{') {
170 end_quote = '}';
171 } else {
172 end_quote = ')';
173 }
174
175 while ((end && (p < end)) || (*p >= ' ')) {
176 if (*p == quote) {
177 p++;
178 depth++;
179 continue;
180 }
181
182 if (*p == end_quote) {
183 p++;
184 depth--;
185 if (!depth) return p - start;
186
187 continue;
188 }
189
190 /*
191 * Nested expansion.
192 */
193 if ((p[0] == '$') || (p[0] == '%')) {
194 if (end && (p + 2) >= end) break;
195
196 if ((p[1] == '{') || ((p[0] == '$') && (p[1] == '('))) {
197 slen = fr_skip_xlat(p, end);
198
199 check:
200 if (slen <= 0) return -(p - start) + slen;
201
202 p += slen;
203 continue;
204 }
205
206 /*
207 * Bare $ or %, just leave it alone.
208 */
209 p++;
210 continue;
211 }
212
213 /*
214 * A quoted string.
215 */
216 if ((*p == '"') || (*p == '\'') || (*p == '`')) {
217 slen = fr_skip_string(p, end);
218 goto check;
219 }
220
221 /*
222 * @todo - bare '(' is a condition or nested
223 * expression. The brackets need to balance
224 * here, too.
225 */
226
227 if (*p != '\\') {
228 p++;
229 continue;
230 }
231
232 if (end && ((p + 2) >= end)) break;
233
234 /*
235 * Escapes here are only one-character escapes.
236 */
237 if (p[1] < ' ') break;
238 p += 2;
239 }
240
241 /*
242 * Unexpected end of xlat
243 */
244 fr_strerror_const("Unexpected end of expansion");
245 return -(p - start);
246}
247
248/** Skip a conditional expression.
249 *
250 * This is a simple "peek ahead" parser which tries to not be wrong. It may accept
251 * some things which will later parse as invalid (e.g. unknown attributes, etc.)
252 * But it also rejects all malformed expressions.
253 *
254 * It's used as a quick hack because the full parser isn't always available.
255 *
256 * @param[in] start start of the condition.
257 * @param[in] end end of the string (or NULL for zero-terminated strings)
258 * @param[in] terminal terminal character(s)
259 * @param[out] eol did the parse error happen at eol?
260 * @return
261 * >0 length of the string which was parsed. *eol is false.
262 * <=0 on error, *eol may be set.
263 */
264ssize_t fr_skip_condition(char const *start, char const *end, bool const terminal[static UINT8_MAX + 1], bool *eol)
265{
266 char const *p = start;
267 bool was_regex = false;
268 int depth = 0;
269 ssize_t slen;
270
271 if (eol) *eol = false;
272
273 /*
274 * Keep parsing the condition until we hit EOS or EOL.
275 */
276 while ((end && (p < end)) || *p) {
277 if (isspace((uint8_t) *p)) {
278 p++;
279 continue;
280 }
281
282 /*
283 * In the configuration files, conditions end with ") {" or just "{"
284 */
285 if ((depth == 0) && terminal[(uint8_t) *p]) {
286 return p - start;
287 }
288
289 /*
290 * "recurse" to get more conditions.
291 */
292 if (*p == '(') {
293 p++;
294 depth++;
295 was_regex = false;
296 continue;
297 }
298
299 if (*p == ')') {
300 if (!depth) {
301 fr_strerror_const("Too many ')'");
302 return -(p - start);
303 }
304
305 p++;
306 depth--;
307 was_regex = false;
308 continue;
309 }
310
311 /*
312 * Parse xlats. They cannot span EOL.
313 */
314 if ((*p == '$') || (*p == '%')) {
315 if (end && ((p + 2) >= end)) {
316 fr_strerror_const("Expansions cannot extend across end of line");
317 return -(p - start);
318 }
319
320 if ((p[1] == '{') || ((p[0] == '$') && (p[1] == '('))) {
321 slen = fr_skip_xlat(p, end);
322
323 check:
324 if (slen <= 0) return -(p - start) + slen;
325
326 p += slen;
327 continue;
328 }
329
330 /*
331 * Bare $ or %, just leave it alone.
332 */
333 p++;
334 was_regex = false;
335 continue;
336 }
337
338 /*
339 * Parse quoted strings. They cannot span EOL.
340 */
341 if ((*p == '"') || (*p == '\'') || (*p == '`') || (was_regex && (*p == '/'))) {
342 was_regex = false;
343
344 slen = fr_skip_string((char const *) p, end);
345 goto check;
346 }
347
348 /*
349 * 192.168/16 is a netmask. So we only
350 * allow regex after a regex operator.
351 *
352 * This isn't perfect, but is good enough
353 * for most purposes.
354 */
355 if ((p[0] == '=') || (p[0] == '!')) {
356 if (end && ((p + 2) >= end)) {
357 fr_strerror_const("Operators cannot extend across end of line");
358 return -(p - start);
359 }
360
361 if (p[1] == '~') {
362 was_regex = true;
363 p += 2;
364 continue;
365 }
366
367 /*
368 * Some other '==' or '!=', just leave it alone.
369 */
370 p++;
371 was_regex = false;
372 continue;
373 }
374
375 /*
376 * Any control characters (other than \t) cause an error.
377 */
378 if (*p < ' ') break;
379
380 was_regex = false;
381
382 /*
383 * Normal characters just get skipped.
384 */
385 if (*p != '\\') {
386 p++;
387 continue;
388 }
389
390 /*
391 * Backslashes at EOL are ignored.
392 */
393 if (end && ((p + 2) >= end)) break;
394
395 /*
396 * Escapes here are only one-character escapes.
397 */
398 if (p[1] < ' ') break;
399 p += 2;
400 }
401
402 /*
403 * We've fallen off of the end of a string. It may be OK?
404 */
405 if (eol) *eol = (depth > 0);
406
407 if (terminal[(uint8_t) *p]) return p - start;
408
409 fr_strerror_const("Unexpected end of condition");
410 return -(p - start);
411}
412
#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
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:264
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:128
#define fr_strerror_const(_msg)
Definition strerror.h:223