The FreeRADIUS server $Id: a4e7f8c94e8038b7627bcf52356b7ed15955c1d7 $
Loading...
Searching...
No Matches
token_tests.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/** Tests for the configuration file tokenizer
18 *
19 * @file src/lib/util/test/token_tests.c
20 *
21 * @copyright 2026 The FreeRADIUS server project
22 */
23#include "acutest_common_init.h"
24#include "acutest_helpers.h"
25
26#include <freeradius-devel/util/token.h>
27
28#include <sys/mman.h>
29
30/** Where a parse ran, and what the parse produced
31 *
32 */
33typedef struct {
34 fr_token_t token; //!< what the parse function returned.
35 char buf[256]; //!< what the parse function wrote.
36 size_t used; //!< how many bytes of the input the parse consumed.
38
39/** Copy a string so that the terminating NUL is the last readable byte
40 *
41 * A read past the NUL lands on an unreadable page and faults, rather than returning
42 * whatever byte happens to follow the string. Every parse in this file runs against a
43 * guarded copy, because a tokenizer which reads one byte too far produces the right answer
44 * on almost every run and the wrong answer on the rest.
45 *
46 * @param[out] base start of the mapping, to pass to guard_free().
47 * @param[in] in to copy.
48 * @return the copy, positioned against the unreadable page.
49 */
50static char *guard_alloc(char **base, char const *in)
51{
52 size_t len = strlen(in);
53 size_t pagesz = test_page_size();
54 char *p;
55
56 *base = mmap(NULL, pagesz * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
57 if (*base == MAP_FAILED) return NULL;
58
59 if (mprotect(*base + pagesz, pagesz, PROT_NONE) < 0) return NULL;
60
61 p = *base + pagesz - (len + 1);
62 memcpy(p, in, len + 1);
63
64 return p;
65}
66
67static void guard_free(char *base)
68{
69 munmap(base, test_page_size() * 2);
70}
71
72/** Run gettoken() over a guarded copy of the input
73 *
74 */
75static parse_result_t test_gettoken(char const *in, bool unescape)
76{
77 parse_result_t result = {};
78 char *base, *start;
79 char const *p;
80
81 start = guard_alloc(&base, in);
82 TEST_ASSERT(start != NULL);
83
84 p = start;
85 result.token = gettoken(&p, result.buf, sizeof(result.buf), unescape);
86 result.used = (size_t) (p - start);
87
88 guard_free(base);
89
90 return result;
91}
92
93/** Run getword() over a guarded copy of the input
94 *
95 * getword() returns 0 at end of line and 1 otherwise, so the rcode goes in `token`.
96 */
97static parse_result_t test_getword(char const *in, bool unescape)
98{
99 parse_result_t result = {};
100 char *base, *start;
101 char const *p;
102
103 start = guard_alloc(&base, in);
104 TEST_ASSERT(start != NULL);
105
106 p = start;
107 result.token = (fr_token_t) getword(&p, result.buf, sizeof(result.buf), unescape);
108 result.used = (size_t) (p - start);
109
110 guard_free(base);
111
112 return result;
113}
114
115/** Run getstring() over a guarded copy of the input
116 *
117 */
118static parse_result_t test_getstring(char const *in, bool unescape)
119{
120 parse_result_t result = {};
121 char *base, *start;
122 char const *p;
123
124 start = guard_alloc(&base, in);
125 TEST_ASSERT(start != NULL);
126
127 p = start;
128 result.token = getstring(&p, result.buf, sizeof(result.buf), unescape);
129 result.used = (size_t) (p - start);
130
131 guard_free(base);
132
133 return result;
134}
135
136/** Run getop() over a guarded copy of the input
137 *
138 */
139static parse_result_t test_getop(char const *in)
140{
141 parse_result_t result = {};
142 char *base, *start;
143 char const *p;
144
145 start = guard_alloc(&base, in);
146 TEST_ASSERT(start != NULL);
147
148 p = start;
149 result.token = getop(&p);
150 result.used = (size_t) (p - start);
151
152 guard_free(base);
153
154 return result;
155}
156
157#define TEST_PARSE(_result, _token, _buf, _used) do { \
158 TEST_CHECK((_result).token == (_token)); \
159 TEST_MSG("token: got %u, expected %u", (_result).token, (fr_token_t)(_token)); \
160 TEST_CHECK(strcmp((_result).buf, (_buf)) == 0); \
161 TEST_MSG("buf: got \"%s\", expected \"%s\"", (_result).buf, (_buf)); \
162 TEST_CHECK((_result).used == (size_t) (_used)); \
163 TEST_MSG("used: got %zu, expected %zu", (_result).used, (size_t) (_used)); \
164} while (0)
165
166/** A triple-quoted string keeps every character between the delimiters
167 *
168 * Skipping three characters for the opening delimiter and then one more ate the first
169 * character of the content. For an input of three quotes and nothing else, the same extra
170 * skip stepped past the terminating NUL.
171 */
172static void test_triple_quote(void)
173{
174 TEST_PARSE(test_gettoken("\"\"\"abc\"\"\"", true), T_DOUBLE_QUOTED_STRING, "abc", 9);
175 TEST_PARSE(test_gettoken("'''abc'''", true), T_SINGLE_QUOTED_STRING, "abc", 9);
176 TEST_PARSE(test_gettoken("```abc```", true), T_BACK_QUOTED_STRING, "abc", 9);
177
178 /*
179 * A triple-quoted string holds quotes which a single-quoted string cannot.
180 */
181 TEST_PARSE(test_gettoken("\"\"\"a\"b\"\"\"", true), T_DOUBLE_QUOTED_STRING, "a\"b", 9);
182
183 /*
184 * Three quotes and nothing else open a string which never ends.
185 */
186 TEST_PARSE(test_gettoken("\"\"\"", true), T_INVALID, "", 0);
187 TEST_PARSE(test_gettoken("\"\"\"abc", true), T_INVALID, "abc", 0);
188
189 /*
190 * Two quotes are an empty string, and are not a triple quote.
191 */
192 TEST_PARSE(test_gettoken("\"\"", true), T_DOUBLE_QUOTED_STRING, "", 2);
193}
194
195/** gettoken() splits on the tokens in the token table
196 *
197 */
199{
200 TEST_PARSE(test_gettoken("hello", true), T_BARE_WORD, "hello", 5);
201
202 /*
203 * The delimiter is left for the next call to read.
204 */
205 TEST_PARSE(test_gettoken("a,b", true), T_BARE_WORD, "a", 1);
206 TEST_PARSE(test_gettoken("a==b", true), T_BARE_WORD, "a", 1);
207 TEST_PARSE(test_gettoken("a b", true), T_BARE_WORD, "a", 2);
208
209 /*
210 * A token which starts the input is returned as that token.
211 */
212 TEST_PARSE(test_gettoken("{", true), T_LCBRACE, "{", 1);
213 TEST_PARSE(test_gettoken("}", true), T_RCBRACE, "}", 1);
214 TEST_PARSE(test_gettoken(",", true), T_COMMA, ",", 1);
215 TEST_PARSE(test_gettoken("==", true), T_OP_CMP_EQ, "==", 2);
216
217 /*
218 * Leading whitespace is skipped, and so is trailing whitespace.
219 */
220 TEST_PARSE(test_gettoken(" abc ", true), T_BARE_WORD, "abc", 9);
221
222 /*
223 * Nothing to read, and whitespace with nothing after it, are both end of line.
224 */
225 TEST_PARSE(test_gettoken("", true), T_EOL, "", 0);
226 TEST_PARSE(test_gettoken(" ", true), T_EOL, "", 3);
227}
228
229/** Each quote character produces its own token type
230 *
231 */
232static void test_gettoken_quotes(void)
233{
234 TEST_PARSE(test_gettoken("\"a b\"", true), T_DOUBLE_QUOTED_STRING, "a b", 5);
235 TEST_PARSE(test_gettoken("'a b'", true), T_SINGLE_QUOTED_STRING, "a b", 5);
236 TEST_PARSE(test_gettoken("`a b`", true), T_BACK_QUOTED_STRING, "a b", 5);
237
238 /*
239 * A quoted string holds the token characters which would otherwise split the input.
240 */
241 TEST_PARSE(test_gettoken("\"a,b==c\"", true), T_DOUBLE_QUOTED_STRING, "a,b==c", 8);
242
243 /*
244 * An empty quoted string is still a string.
245 */
247}
248
249/** gettoken() turns escape sequences into the characters the sequences name
250 *
251 */
252static void test_gettoken_unescape(void)
253{
254 TEST_PARSE(test_gettoken("\"a\\nb\"", true), T_DOUBLE_QUOTED_STRING, "a\nb", 6);
255 TEST_PARSE(test_gettoken("\"a\\rb\"", true), T_DOUBLE_QUOTED_STRING, "a\rb", 6);
256 TEST_PARSE(test_gettoken("\"a\\tb\"", true), T_DOUBLE_QUOTED_STRING, "a\tb", 6);
257
258 /*
259 * Three octal digits are one character. 101 octal is 'A'.
260 */
261 TEST_PARSE(test_gettoken("\"a\\101b\"", true), T_DOUBLE_QUOTED_STRING, "aAb", 8);
262
263 /*
264 * Fewer than three octal digits, or a value above 0377, is not an octal escape. The
265 * backslash yields the next character, and no character after the digits is lost.
266 */
267 TEST_PARSE(test_gettoken("\"a\\1bc\"", true), T_DOUBLE_QUOTED_STRING, "a1bc", 7);
268 TEST_PARSE(test_gettoken("\"a\\777b\"", true), T_DOUBLE_QUOTED_STRING, "a777b", 8);
269
270 /*
271 * An escaped quote does not end the string.
272 */
273 TEST_PARSE(test_gettoken("\"a\\\"b\"", true), T_DOUBLE_QUOTED_STRING, "a\"b", 6);
274
275 /*
276 * A backslash in front of anything else yields that character.
277 */
278 TEST_PARSE(test_gettoken("\"a\\qb\"", true), T_DOUBLE_QUOTED_STRING, "aqb", 6);
279 TEST_PARSE(test_gettoken("\"a\\\\b\"", true), T_DOUBLE_QUOTED_STRING, "a\\b", 6);
280}
281
282/** Without unescaping, gettoken() keeps the backslash
283 *
284 * The one exception is a backslash in front of the quote character, which the tokenizer
285 * still has to remove so that the quote does not end the string.
286 */
288{
289 TEST_PARSE(test_gettoken("\"a\\nb\"", false), T_DOUBLE_QUOTED_STRING, "a\\nb", 6);
290 TEST_PARSE(test_gettoken("\"a\\101b\"", false), T_DOUBLE_QUOTED_STRING, "a\\101b", 8);
291
292 TEST_PARSE(test_gettoken("\"a\\\"b\"", false), T_DOUBLE_QUOTED_STRING, "a\"b", 6);
293}
294
295/** A string which never ends is an error, and the input pointer does not move
296 *
297 */
299{
300 TEST_PARSE(test_gettoken("\"abc", true), T_INVALID, "abc", 0);
301 TEST_PARSE(test_gettoken("'abc", true), T_INVALID, "abc", 0);
302
303 /*
304 * The closing quote is escaped, so the string still never ends.
305 */
306 TEST_PARSE(test_gettoken("\"abc\\\"", true), T_INVALID, "abc\"", 0);
307
308 /*
309 * A backslash with nothing after it is an error rather than an escape. getthing()
310 * terminates the output buffer before returning, so the caller may read the buffer
311 * even on the error paths.
312 */
313 TEST_PARSE(test_gettoken("\"abc\\", true), T_INVALID, "abc", 0);
314}
315
316/** An input longer than the output buffer stops at the end of the output buffer
317 *
318 */
320{
321 char buf[4];
322 char *base, *start;
323 char const *p;
324 fr_token_t token;
325
326 /*
327 * A bare word is truncated to the space available, and is not an error.
328 */
329 start = guard_alloc(&base, "abcdefgh");
330 TEST_ASSERT(start != NULL);
331 p = start;
332 token = gettoken(&p, buf, sizeof(buf), true);
333 TEST_CHECK(token == T_BARE_WORD);
334 TEST_CHECK(strcmp(buf, "abc") == 0);
335 TEST_MSG("got \"%s\"", buf);
336 guard_free(base);
337
338 /*
339 * A quoted string is an error, because the closing quote is never reached.
340 */
341 start = guard_alloc(&base, "\"abcdefgh\"");
342 TEST_ASSERT(start != NULL);
343 p = start;
344 token = gettoken(&p, buf, sizeof(buf), true);
345 TEST_CHECK(token == T_INVALID);
346 TEST_MSG("got token %u", token);
347 guard_free(base);
348}
349
350/** getword() reads to whitespace, and ignores the tokens which split gettoken()
351 *
352 */
354{
355 /*
356 * getword() returns 1 when the call read a word, and 0 at end of line.
357 */
358 TEST_PARSE(test_getword("hello", true), 1, "hello", 5);
359
360 /*
361 * gettoken() stops at the "==". getword() does not.
362 */
363 TEST_PARSE(test_getword("a==b", true), 1, "a==b", 4);
364 TEST_PARSE(test_getword("a;b", true), 1, "a;b", 3);
365
366 /*
367 * The comma is the exception. getthing() tests for the comma outside the block
368 * which tests the token list, so the comma ends a word for getword() as well as for
369 * gettoken().
370 */
371 TEST_PARSE(test_getword("a,b", true), 1, "a", 1);
372
373 /*
374 * Whitespace still ends the word.
375 */
376 TEST_PARSE(test_getword("a b", true), 1, "a", 2);
377
378 TEST_PARSE(test_getword("", true), 0, "", 0);
379
380 /*
381 * getword() reads quoted strings, which is the path that $TEMPLATE uses.
382 */
383 TEST_PARSE(test_getword("\"a b\"", true), 1, "a b", 5);
384 TEST_PARSE(test_getword("\"\"\"abc\"\"\"", true), 1, "abc", 9);
385}
386
387/** getop() accepts an assignment or comparison operator and rejects everything else
388 *
389 */
390static void test_getop_operators(void)
391{
392 TEST_CHECK(test_getop("=").token == T_OP_EQ);
393 TEST_CHECK(test_getop(":=").token == T_OP_SET);
394 TEST_CHECK(test_getop("+=").token == T_OP_ADD_EQ);
395 TEST_CHECK(test_getop("-=").token == T_OP_SUB_EQ);
396 TEST_CHECK(test_getop("==").token == T_OP_CMP_EQ);
397 TEST_CHECK(test_getop("!=").token == T_OP_NE);
398 TEST_CHECK(test_getop("<=").token == T_OP_LE);
399 TEST_CHECK(test_getop(">=").token == T_OP_GE);
400 TEST_CHECK(test_getop("=~").token == T_OP_REG_EQ);
401 TEST_CHECK(test_getop("!~").token == T_OP_REG_NE);
402
403 /*
404 * A word is not an operator, and neither is a brace.
405 */
406 TEST_CHECK(test_getop("hello").token == T_INVALID);
407 TEST_CHECK(test_getop("{").token == T_INVALID);
408 TEST_CHECK(test_getop("").token == T_INVALID);
409}
410
411/** getstring() reads a quoted string as a string, and anything else as a word
412 *
413 */
414static void test_getstring_quoting(void)
415{
416 TEST_PARSE(test_getstring("\"a b\"", true), T_DOUBLE_QUOTED_STRING, "a b", 5);
417 TEST_PARSE(test_getstring("'a b'", true), T_SINGLE_QUOTED_STRING, "a b", 5);
418 TEST_PARSE(test_getstring("`a b`", true), T_BACK_QUOTED_STRING, "a b", 5);
419
420 /*
421 * An unquoted string reaches getthing() with the token list turned off, so the
422 * tokens which split gettoken() do not split getstring().
423 */
424 TEST_PARSE(test_getstring("a=b", true), T_BARE_WORD, "a=b", 3);
425
426 TEST_PARSE(test_getstring("a b", true), T_BARE_WORD, "a", 2);
427 TEST_PARSE(test_getstring("", true), T_EOL, "", 0);
428}
429
430/** getstring() rejects a NULL pointer rather than following the pointer
431 *
432 */
433static void test_getstring_null(void)
434{
435 char buf[16];
436 char const *p = NULL;
437
438 TEST_CHECK(getstring(NULL, buf, sizeof(buf), true) == T_INVALID);
439 TEST_CHECK(getstring(&p, buf, sizeof(buf), true) == T_INVALID);
440
441 p = "hello";
442 TEST_CHECK(getstring(&p, NULL, 16, true) == T_INVALID);
443}
444
445/** fr_token_name() names the tokens which the tokenizer matches as tokens
446 *
447 * The word and string tokens are not in that table, so fr_token_name() does not name the
448 * word and string tokens. Use fr_token_to_enum_str() for those.
449 */
450static void test_token_name(void)
451{
452 TEST_CHECK(strcmp(fr_token_name(T_OP_EQ), "=") == 0);
453 TEST_CHECK(strcmp(fr_token_name(T_LCBRACE), "{") == 0);
454 TEST_CHECK(strcmp(fr_token_name(T_OP_CMP_EQ), "==") == 0);
455
456 TEST_CHECK(strcmp(fr_token_name(T_BARE_WORD), "<INVALID>") == 0);
457 TEST_CHECK(strcmp(fr_token_name(9999), "<INVALID>") == 0);
458 TEST_CHECK(strcmp(fr_token_name(-1), "<INVALID>") == 0);
459}
460
461/** fr_token_to_enum_str() names a token with the name of the C enumeration value
462 *
463 */
464static void test_token_to_enum_str(void)
465{
466 TEST_CHECK(strcmp(fr_token_to_enum_str(T_OP_EQ), "T_OP_EQ") == 0);
467 TEST_CHECK(strcmp(fr_token_to_enum_str(T_BARE_WORD), "T_BARE_WORD") == 0);
468 TEST_CHECK(strcmp(fr_token_to_enum_str(T_DOUBLE_QUOTED_STRING), "T_DOUBLE_QUOTED_STRING") == 0);
469}
470
471/** fr_token_from_quote_enum_str() turns an enumeration name back into a token
472 *
473 */
475{
481
482 /*
483 * The table holds the quote tokens only, so an operator name is not in the table.
484 */
486
487 /*
488 * An unknown name and a NULL name both return the default which the caller gave.
489 */
493}
494
496 { "triple_quote", test_triple_quote },
497 { "gettoken_delimiters", test_gettoken_delimiters },
498 { "gettoken_quotes", test_gettoken_quotes },
499 { "gettoken_unescape", test_gettoken_unescape },
500 { "gettoken_no_unescape", test_gettoken_no_unescape },
501 { "gettoken_unterminated", test_gettoken_unterminated },
502 { "gettoken_truncation", test_gettoken_truncation },
503 { "getword_no_delimiters", test_getword_no_delimiters },
504 { "getop_operators", test_getop_operators },
505 { "getstring_quoting", test_getstring_quoting },
506 { "getstring_null", test_getstring_null },
507 { "token_name", test_token_name },
508 { "token_to_enum_str", test_token_to_enum_str },
509 { "token_from_quote_enum_str", test_token_from_quote_enum_str },
510
512};
#define TEST_CHECK(cond)
Definition acutest.h:101
#define TEST_ASSERT(cond)
Definition acutest.h:125
#define TEST_TERMINATOR
Definition acutest.h:77
#define TEST_MSG(...)
Definition acutest.h:234
Common initialisation for acutest binaries.
static size_t test_page_size(void)
Return the system page size, aborting the test if sysconf() fails.
unsigned long int size_t
char const * fr_token_name(int token)
Definition token.c:582
fr_token_t gettoken(char const **ptr, char *buf, int buflen, bool unescape)
Definition token.c:539
char const * fr_token_to_enum_str(fr_token_t t)
Return the source-identifier name for a token (e.g.
Definition token.c:117
fr_token_t getop(char const **ptr)
Definition token.c:547
fr_token_t getstring(char const **ptr, char *buf, int buflen, bool unescape)
Definition token.c:563
int getword(char const **ptr, char *buf, int buflen, bool unescape)
Definition token.c:530
fr_token_t fr_token_from_quote_enum_str(char const *s, fr_token_t dflt)
Look up a quote token by its source-identifier name.
Definition token.c:137
@ T_OP_SUB_EQ
Definition token.h:68
@ T_INVALID
Definition token.h:37
@ T_RCBRACE
Definition token.h:40
@ T_EOL
Definition token.h:38
@ T_SINGLE_QUOTED_STRING
Definition token.h:120
@ T_BARE_WORD
Definition token.h:118
@ T_OP_EQ
Definition token.h:81
@ T_BACK_QUOTED_STRING
Definition token.h:121
@ T_OP_SET
Definition token.h:82
@ T_OP_NE
Definition token.h:95
@ T_OP_ADD_EQ
Definition token.h:67
@ T_LCBRACE
Definition token.h:39
@ T_OP_REG_EQ
Definition token.h:100
@ T_DOUBLE_QUOTED_STRING
Definition token.h:119
@ T_OP_CMP_EQ
Definition token.h:104
@ T_OP_LE
Definition token.h:98
@ T_OP_GE
Definition token.h:96
@ T_SOLIDUS_QUOTED_STRING
Definition token.h:122
@ T_OP_REG_NE
Definition token.h:101
@ T_COMMA
Definition token.h:43
enum fr_token fr_token_t
static void test_gettoken_unescape(void)
gettoken() turns escape sequences into the characters the sequences name
TEST_LIST
static void test_getstring_null(void)
getstring() rejects a NULL pointer rather than following the pointer
static void test_token_name(void)
fr_token_name() names the tokens which the tokenizer matches as tokens
static void test_gettoken_truncation(void)
An input longer than the output buffer stops at the end of the output buffer.
static void test_token_from_quote_enum_str(void)
fr_token_from_quote_enum_str() turns an enumeration name back into a token
size_t used
how many bytes of the input the parse consumed.
Definition token_tests.c:36
static void test_gettoken_delimiters(void)
gettoken() splits on the tokens in the token table
static void test_getop_operators(void)
getop() accepts an assignment or comparison operator and rejects everything else
static parse_result_t test_getstring(char const *in, bool unescape)
Run getstring() over a guarded copy of the input.
static void test_gettoken_no_unescape(void)
Without unescaping, gettoken() keeps the backslash.
fr_token_t token
what the parse function returned.
Definition token_tests.c:34
static void test_getword_no_delimiters(void)
getword() reads to whitespace, and ignores the tokens which split gettoken()
static void test_triple_quote(void)
A triple-quoted string keeps every character between the delimiters.
static void guard_free(char *base)
Definition token_tests.c:67
static parse_result_t test_gettoken(char const *in, bool unescape)
Run gettoken() over a guarded copy of the input.
Definition token_tests.c:75
static void test_gettoken_quotes(void)
Each quote character produces its own token type.
static char * guard_alloc(char **base, char const *in)
Copy a string so that the terminating NUL is the last readable byte.
Definition token_tests.c:50
#define TEST_PARSE(_result, _token, _buf, _used)
static void test_gettoken_unterminated(void)
A string which never ends is an error, and the input pointer does not move.
char buf[256]
what the parse function wrote.
Definition token_tests.c:35
static parse_result_t test_getword(char const *in, bool unescape)
Run getword() over a guarded copy of the input.
Definition token_tests.c:97
static void test_token_to_enum_str(void)
fr_token_to_enum_str() names a token with the name of the C enumeration value
static void test_getstring_quoting(void)
getstring() reads a quoted string as a string, and anything else as a word
static parse_result_t test_getop(char const *in)
Run getop() over a guarded copy of the input.
Where a parse ran, and what the parse produced.
Definition token_tests.c:33
static fr_sbuff_err_t char size_t fr_sbuff_t * in
Definition value.h:1061
static fr_sbuff_err_t char size_t * len
Definition value.h:1061