1 /*
2 * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
3 * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8 #include <ctype.h>
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <fnmatch.h>
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <unistd.h>
16
17 #include "bsdconf.h"
18 #include "bsdconf_internal.h"
19
20 /*
21 * Search for a config option (struct bsdconf_option) in the array of config
22 * options, returning the struct whose directive matches the given parameter.
23 * If no match is found, NULL is returned.
24 *
25 * This is to eliminate dependency on the index position of an item in the
26 * array, since the index position is more apt to be changed as code grows.
27 */
28 struct bsdconf_option *
bsdconf_get_option(struct bsdconf_option options[],const char * directive)29 bsdconf_get_option(struct bsdconf_option options[], const char *directive)
30 {
31 uint32_t n;
32
33 if (options == NULL || directive == NULL)
34 return (NULL);
35
36 for (n = 0; options[n].directive != NULL; n++)
37 if (strcmp(options[n].directive, directive) == 0)
38 return (&options[n]);
39
40 return (NULL);
41 }
42
43 /*
44 * Strip one layer of surrounding double-quotes from `value' in place (the
45 * parser preserves quotes so that values round-trip losslessly; see
46 * bsdconf_fparse() below). Returns `value' for convenience. If the value is
47 * not a quoted string, it is returned unmodified.
48 */
49 char *
bsdconf_unquote(char * value)50 bsdconf_unquote(char *value)
51 {
52 size_t len;
53
54 if (value == NULL || (len = strlen(value)) < 2)
55 return (value);
56 if (value[0] != '"' || value[len - 1] != '"')
57 return (value);
58
59 memmove(value, value + 1, len - 2);
60 value[len - 2] = '\0';
61
62 return (value);
63 }
64
65 /*
66 * Copy the remaining contents of the open file descriptor `fd' to an
67 * unlinked temporary file and return a seekable descriptor referencing it
68 * (which the caller must close(2); the backing storage is reclaimed then).
69 * This adapts input that cannot seek -- a pipe or socket, standard input
70 * included -- for the scanner in bsdconf_fparse() below, which seeks
71 * liberally. Returns the new descriptor on success; otherwise returns -1
72 * and errno should be consulted.
73 */
74 int
bsdconf_spool(int fd)75 bsdconf_spool(int fd)
76 {
77 FILE *tmp;
78 int error;
79 int newfd;
80 int tfd;
81 ssize_t r;
82 char buf[8192];
83
84 if ((tmp = tmpfile()) == NULL)
85 return (-1);
86 tfd = fileno(tmp);
87
88 for (;;) {
89 r = read(fd, buf, sizeof(buf));
90 if (r < 0) {
91 if (errno == EINTR)
92 continue;
93 goto fail;
94 }
95 if (r == 0)
96 break;
97 if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
98 goto fail;
99 }
100 if (lseek(tfd, 0, SEEK_SET) == -1)
101 goto fail;
102
103 /* Detach the descriptor from the stream before closing it */
104 if ((newfd = dup(tfd)) == -1)
105 goto fail;
106 fclose(tmp);
107 return (newfd);
108
109 fail:
110 error = errno; /* preserve errno across fclose(3) */
111 fclose(tmp);
112 errno = error;
113 return (-1);
114 }
115
116 /*
117 * Read one byte into `*p', restarting on EINTR. Returns 1 on success, 0 on
118 * EOF, or -1 on error (with errno set). Callers must treat a negative return
119 * as failure: a loop conditioned only on `r != 0' spins forever on error
120 * because read(2) returns -1, and a length counter in such a loop can grow
121 * without bound (see the directive scan in bsdconf_fparse() below).
122 */
123 static ssize_t
bsdconf_read1(int fd,char * p)124 bsdconf_read1(int fd, char *p)
125 {
126 ssize_t r;
127
128 do {
129 r = read(fd, p, 1);
130 } while (r < 0 && errno == EINTR);
131 return (r);
132 }
133
134 /*
135 * Read exactly `n' bytes into `buf', restarting on EINTR. Returns 0 on
136 * success, or -1 on error / premature EOF (with errno set; EIO for a short
137 * read after the caller measured a length on a seekable descriptor).
138 */
139 static int
bsdconf_readn(int fd,void * buf,size_t n)140 bsdconf_readn(int fd, void *buf, size_t n)
141 {
142 char *p = buf;
143 size_t off = 0;
144 ssize_t r;
145
146 while (off < n) {
147 r = read(fd, p + off, n - off);
148 if (r < 0) {
149 if (errno == EINTR)
150 continue;
151 return (-1);
152 }
153 if (r == 0) {
154 errno = EIO;
155 return (-1);
156 }
157 off += (size_t)r;
158 }
159 return (0);
160 }
161
162 /*
163 * Advance past horizontal whitespace (spaces and tabs, not newline).
164 * Updates `*r' and the byte in `*p'. Returns 0 on success, or -1 on
165 * read error (errno set).
166 */
167 static int
bsdconf_skip_hspace(int fd,char * p,ssize_t * r)168 bsdconf_skip_hspace(int fd, char *p, ssize_t *r)
169 {
170
171 while (*r > 0 && isspace((unsigned char)*p) && *p != '\n') {
172 *r = bsdconf_read1(fd, p);
173 if (*r < 0)
174 return (-1);
175 }
176 return (0);
177 }
178
179 /*
180 * Truncate trailing whitespace from a NUL-terminated string whose end
181 * (the NUL) is at `end'. Returns a pointer to the last remaining
182 * character, or to `value' when the string is empty.
183 */
184 static char *
bsdconf_rtrim_ws(char * value,char * end)185 bsdconf_rtrim_ws(char *value, char *end)
186 {
187 char *t = end;
188
189 while (t > value && isspace((unsigned char)*--t))
190 *t = '\0';
191 return (t);
192 }
193
194 /*
195 * Drop a trailing inline `#' or unescaped `;' that rode along with the
196 * value (historic figpar behavior), then trim again. `ecomment' is set
197 * when the end-key scan stopped on an unquoted `#'.
198 */
199 static char *
bsdconf_trim_value_key(char * value,char * t,bool ecomment,bool bsemicolon)200 bsdconf_trim_value_key(char *value, char *t, bool ecomment, bool bsemicolon)
201 {
202 uint32_t x;
203
204 if (ecomment && t > value && *t == '#') {
205 *t = '\0';
206 return (bsdconf_rtrim_ws(value, t));
207 }
208 if (bsemicolon && t > value && *t == ';') {
209 for (x = 0; t - x > value && *(t - x - 1) == '\\'; x++)
210 ;
211 if ((x & 1) == 0) {
212 *t = '\0';
213 return (bsdconf_rtrim_ws(value, t));
214 }
215 }
216 return (t);
217 }
218
219 /*
220 * Invoke the unknown-directive call-back with a stack-local option that
221 * carries the statement's assignment operator (there is no matched
222 * options[] slot to hang it on). Returns the call-back's result.
223 */
224 static int
bsdconf_call_unknown(int (* unknown)(struct bsdconf_option * option,uint32_t line,char * directive,char * value),enum bsdconf_op op,uint32_t dline,char * directive,char * value)225 bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
226 uint32_t line, char *directive, char *value), enum bsdconf_op op,
227 uint32_t dline, char *directive, char *value)
228 {
229 struct bsdconf_option unk;
230
231 memset(&unk, 0, sizeof(unk));
232 unk.op = op;
233 return (unknown(&unk, dline, directive, value));
234 }
235
236 /*
237 * Scan from the current byte in `*p' to the end of the value. Handles
238 * quotes, escaped end-keys, inline comments, and semicolon terminators.
239 * On return, `*p' holds the terminating key (or is at EOF), and `*r',
240 * `*line', `*comment', and `*ecomment' are updated. Returns 0 on
241 * success, or -1 on seek/read error (errno set).
242 */
243 static int
bsdconf_scan_value_end(int fd,char * p,ssize_t * r,uint32_t * line,uint8_t * comment,uint8_t * ecomment,bool bsemicolon)244 bsdconf_scan_value_end(int fd, char *p, ssize_t *r, uint32_t *line,
245 uint8_t *comment, uint8_t *ecomment, bool bsemicolon)
246 {
247 uint8_t end = 0;
248 uint8_t quote = 0;
249 uint32_t n;
250 off_t charpos;
251
252 *ecomment = 0;
253 while (*r > 0 && end == 0) {
254 /* Advance to the next character if we know we can */
255 if (*p != '\"' && *p != '#' && *p != '\n' &&
256 (!bsemicolon || *p != ';')) {
257 *r = bsdconf_read1(fd, p);
258 if (*r < 0)
259 return (-1);
260 continue;
261 }
262
263 /*
264 * If we get this far, we've hit an end-key
265 */
266
267 /* Get the current offset */
268 if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
269 return (-1);
270 charpos--;
271
272 /*
273 * Go back so we can read the character before the key to
274 * check if the character is escaped (which means we should
275 * continue).
276 */
277 if (lseek(fd, -2, SEEK_CUR) == -1)
278 return (-1);
279 *r = bsdconf_read1(fd, p);
280 if (*r < 0)
281 return (-1);
282
283 /*
284 * Count how many backslashes there are (an odd number means
285 * the key is escaped, even means otherwise).
286 */
287 for (n = 1; *r > 0 && *p == '\\'; n++) {
288 /* Move back another offset to read */
289 if (lseek(fd, -2, SEEK_CUR) == -1)
290 return (-1);
291 *r = bsdconf_read1(fd, p);
292 if (*r < 0)
293 return (-1);
294 }
295
296 /* Move offset back to the key and read it */
297 if (lseek(fd, charpos, SEEK_SET) == -1)
298 return (-1);
299 *r = bsdconf_read1(fd, p);
300 if (*r < 0)
301 return (-1);
302
303 /*
304 * If an even number of backslashes was counted meaning key
305 * is not escaped, we should evaluate what to do.
306 */
307 if ((n & 1) == 1) {
308 switch (*p) {
309 case '\"':
310 /*
311 * Flag current sequence of characters to
312 * follow as being quoted (hashes are not
313 * considered comments).
314 */
315 quote = !quote;
316 break;
317 case '#':
318 /*
319 * If we aren't in a quoted series, we just
320 * hit an inline comment and have found the
321 * end of the value. Flag the remainder of
322 * the line as a comment so it is not
323 * mistaken for a new directive.
324 */
325 if (!quote) {
326 *ecomment = *comment = 1;
327 end = 1;
328 }
329 break;
330 case '\n':
331 /*
332 * Newline characters must always be escaped,
333 * whether inside a quoted series or not,
334 * otherwise they terminate the value.
335 */
336 (*line)++;
337 end = 1;
338 /* FALLTHROUGH */
339 case ';':
340 if (!quote && bsemicolon)
341 end = 1;
342 break;
343 }
344 } else if (*p == '\n')
345 /* Escaped newline character. increment */
346 (*line)++;
347
348 /* Advance to the next character */
349 *r = bsdconf_read1(fd, p);
350 if (*r < 0)
351 return (-1);
352 }
353 return (0);
354 }
355
356 /*
357 * Parse the configuration data on the open file descriptor `fd' and execute
358 * the `parse' call-back functions for any directives defined by the array of
359 * config options (first argument).
360 *
361 * For unknown directives that are encountered, you can optionally pass a
362 * call-back function for the third argument to be called for unknowns.
363 *
364 * The scanner requires a seekable descriptor; input that cannot seek (a
365 * pipe or socket, standard input included) is detected up front and spooled
366 * through bsdconf_spool() above, parsed from the temporary, and costs one
367 * transient copy of the data. The descriptor is left positioned at
368 * end-of-file (non-seekable input is left drained) and remains open (the
369 * caller retains ownership).
370 *
371 * Returns zero on success; otherwise returns -1 (or the non-zero result of a
372 * call-back) and errno should be consulted.
373 */
374 int
bsdconf_fparse(struct bsdconf_option options[],int fd,int (* unknown)(struct bsdconf_option * option,uint32_t line,char * directive,char * value),uint16_t processing_options)375 bsdconf_fparse(struct bsdconf_option options[], int fd,
376 int (*unknown)(struct bsdconf_option *option, uint32_t line,
377 char *directive, char *value), uint16_t processing_options)
378 {
379 bool bequals;
380 bool bsemicolon;
381 bool case_sensitive;
382 bool operator_equals;
383 bool require_equals;
384 bool strict_equals;
385 uint8_t comment = 0;
386 uint8_t ecomment;
387 uint8_t found;
388 uint8_t have_equals = 0;
389 char p[2];
390 char *directive = NULL;
391 char *t;
392 char *value = NULL;
393 enum bsdconf_op op;
394 int error;
395 int rv = 0;
396 int spoolfd = -1;
397 ssize_t r = 1;
398 uint32_t dline;
399 uint32_t dsize = 0;
400 uint32_t line = 1;
401 uint32_t n;
402 uint32_t vsize = 0;
403 uint32_t x;
404 off_t charpos;
405 off_t curpos;
406
407 /* Sanity check: if no options and no unknown function, return */
408 if (options == NULL && unknown == NULL) {
409 errno = EINVAL;
410 return (-1);
411 }
412
413 /* Spool input that cannot seek (see bsdconf_spool() above) */
414 if (lseek(fd, 0, SEEK_CUR) == -1) {
415 if (errno != ESPIPE)
416 return (-1);
417 if ((spoolfd = bsdconf_spool(fd)) == -1)
418 return (-1);
419 fd = spoolfd;
420 }
421
422 /* Processing options */
423 bequals = processing_options & BSDCONF_BREAK_ON_EQUALS;
424 bsemicolon = processing_options & BSDCONF_BREAK_ON_SEMICOLON;
425 case_sensitive = processing_options & BSDCONF_CASE_SENSITIVE;
426 operator_equals = processing_options & BSDCONF_OPERATOR_EQUALS;
427 require_equals = processing_options & BSDCONF_REQUIRE_EQUALS;
428 strict_equals = processing_options & BSDCONF_STRICT_EQUALS;
429
430 /* Read the file until EOF */
431 while (r > 0) {
432 r = bsdconf_read1(fd, p);
433 if (r < 0)
434 goto fail;
435
436 /* Skip to the beginning of a directive */
437 while (r > 0 && (isspace((unsigned char)*p) || *p == '#' ||
438 comment || (bsemicolon && *p == ';'))) {
439 if (*p == '#')
440 comment = 1;
441 else if (*p == '\n') {
442 comment = 0;
443 line++;
444 }
445 r = bsdconf_read1(fd, p);
446 if (r < 0)
447 goto fail;
448 }
449 /* Test for EOF; if EOF then no directive was found */
450 if (r == 0)
451 goto cleanup;
452
453 /* Record the line number the directive appears on */
454 dline = line;
455
456 /* Get the current offset */
457 if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
458 goto fail;
459 curpos--;
460
461 /* Find the length of the directive */
462 for (n = 0; r > 0; n++) {
463 if (isspace((unsigned char)*p))
464 break;
465 if (bequals && *p == '=') {
466 have_equals = 1;
467 break;
468 }
469 if (bsemicolon && *p == ';')
470 break;
471 r = bsdconf_read1(fd, p);
472 if (r < 0)
473 goto fail;
474 }
475
476 /* Test for EOF, if EOF then no directive was found */
477 if (n == 0 && r == 0)
478 goto cleanup;
479
480 /* Go back to the beginning of the directive */
481 if (lseek(fd, curpos, SEEK_SET) == -1)
482 goto fail;
483
484 /*
485 * Allocate and read the directive into memory. The buffer
486 * must be grown on the first pass (directive == NULL) even
487 * when the name is empty (a line beginning with `='), lest
488 * the string terminator below store through a NULL pointer.
489 */
490 if (directive == NULL || n > dsize) {
491 if ((t = realloc(directive, n + 1)) == NULL)
492 goto fail;
493 directive = t;
494 dsize = n;
495 }
496 if (bsdconf_readn(fd, directive, n) != 0)
497 goto fail;
498
499 /* Advance beyond the equals sign if appropriate/desired */
500 if (bequals && *p == '=') {
501 if (lseek(fd, 1, SEEK_CUR) != -1) {
502 r = bsdconf_read1(fd, p);
503 if (r < 0)
504 goto fail;
505 }
506 if (strict_equals && isspace((unsigned char)*p))
507 *p = '\n';
508 }
509
510 /* Terminate the string */
511 directive[n] = '\0';
512
513 /*
514 * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off
515 * the tail of the directive if requested. The operator
516 * character rode along with the directive because only the
517 * `=' terminates the directive scan (above).
518 */
519 op = have_equals ? BSDCONF_OP_ASSIGN : BSDCONF_OP_DEFAULT;
520 if (operator_equals && have_equals && n > 1) {
521 switch (directive[n - 1]) {
522 case '+': op = BSDCONF_OP_APPEND; break;
523 case '?': op = BSDCONF_OP_COND; break;
524 case ':': op = BSDCONF_OP_EXPAND; break;
525 case '!': op = BSDCONF_OP_SHELL; break;
526 }
527 if (op != BSDCONF_OP_ASSIGN)
528 directive[--n] = '\0';
529 }
530
531 /* Convert directive to lower case before comparison */
532 if (!case_sensitive)
533 bsdconf_strtolower(directive);
534
535 /* Move to what may be the start of the value */
536 if (!(bsemicolon && *p == ';') &&
537 !(strict_equals && *p == '=')) {
538 if (bsdconf_skip_hspace(fd, p, &r) != 0)
539 goto fail;
540 }
541
542 /* An equals sign may have stopped us, should we eat it? */
543 if (r > 0 && bequals && *p == '=' && !strict_equals) {
544 have_equals = 1;
545 r = bsdconf_read1(fd, p);
546 if (r < 0)
547 goto fail;
548 if (bsdconf_skip_hspace(fd, p, &r) != 0)
549 goto fail;
550 }
551
552 /* If no value, allocate a dummy value and jump to action */
553 if (r == 0 || *p == '\n' || *p == '#' ||
554 (bsemicolon && *p == ';')) {
555 /* Count the consumed terminator if a newline */
556 if (r > 0 && *p == '\n')
557 line++;
558 /* Flag a trailing comment so it is skipped */
559 if (r > 0 && *p == '#')
560 comment = 1;
561 /* Initialize the value if not already done */
562 if (value == NULL && (value = malloc(1)) == NULL)
563 goto fail;
564 value[0] = '\0';
565 goto call_function;
566 }
567
568 /* Get the current offset */
569 if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
570 goto fail;
571 curpos--;
572
573 /* Find the end of the value */
574 if (bsdconf_scan_value_end(fd, p, &r, &line, &comment,
575 &ecomment, bsemicolon) != 0)
576 goto fail;
577
578 /* Get the current offset */
579 if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
580 goto fail;
581
582 /* Get the length of the value */
583 n = (uint32_t)(charpos - curpos);
584 if (r > 0) /* more to read, but don't read ending key */
585 n--;
586
587 /* Move offset back to the beginning of the value */
588 if (lseek(fd, curpos, SEEK_SET) == -1)
589 goto fail;
590
591 /* Allocate and read the value into memory */
592 if (n > vsize) {
593 if ((t = realloc(value, n + 1)) == NULL)
594 goto fail;
595 value = t;
596 vsize = n;
597 }
598 if (bsdconf_readn(fd, value, n) != 0)
599 goto fail;
600
601 /* Terminate the string */
602 value[n] = '\0';
603
604 /* Cut trailing whitespace and a trailing `#' / `;' key */
605 t = bsdconf_rtrim_ws(value, value + n);
606 t = bsdconf_trim_value_key(value, t, ecomment != 0,
607 bsemicolon);
608
609 /* Escape the escaped quotes (see bsdconf_string.c) */
610 x = bsdconf_strcount(value, "\\\"");
611 if (x != 0 && (n + x) > vsize) {
612 if ((t = realloc(value, n + x + 1)) == NULL)
613 goto fail;
614 value = t;
615 vsize = n + x;
616 }
617 if (bsdconf_replaceall(value, vsize + 1, "\\\"", "\\\\\"") < 0)
618 goto fail;
619
620 /* Remove all escaped newline characters */
621 if (bsdconf_replaceall(value, vsize + 1, "\\\n", "") < 0)
622 goto fail;
623
624 /* Resolve escape sequences */
625 bsdconf_strunexpand(value, value);
626
627 call_function:
628 /* Abort if we're seeking only assignments */
629 if (require_equals && !have_equals) {
630 errno = EINVAL;
631 goto fail;
632 }
633
634 found = have_equals = 0; /* reset */
635
636 /*
637 * Report the statement's assignment operator through a
638 * stack-local option when invoking the unknown call-back
639 * (there is no matched options[] slot to hang it on).
640 */
641 if (options == NULL && unknown != NULL) {
642 error = bsdconf_call_unknown(unknown, op, dline,
643 directive, value);
644 if (error != 0) {
645 rv = error;
646 goto cleanup;
647 }
648 continue;
649 }
650
651 /* Loop through the array looking for a match */
652 for (n = 0; options[n].directive != NULL; n++) {
653 error = fnmatch(options[n].directive, directive,
654 FNM_NOESCAPE);
655 if (error == 0) {
656 found = 1;
657 /* Call function for array index item */
658 options[n].op = op;
659 if (options[n].parse != NULL) {
660 error = options[n].parse(&options[n],
661 dline, directive, value);
662 if (error != 0) {
663 rv = error;
664 goto cleanup;
665 }
666 }
667 } else if (error != FNM_NOMATCH) {
668 /* An error has occurred */
669 errno = EINVAL;
670 goto fail;
671 }
672 }
673 if (!found && unknown != NULL) {
674 /*
675 * No match was found for the value we read from the
676 * file; call function designated for unknown values.
677 */
678 error = bsdconf_call_unknown(unknown, op, dline,
679 directive, value);
680 if (error != 0) {
681 rv = error;
682 goto cleanup;
683 }
684 }
685 }
686
687 goto cleanup;
688
689 fail:
690 rv = -1;
691
692 cleanup:
693 x = errno; /* preserve errno across free(3) and close(2) */
694 if (spoolfd != -1)
695 close(spoolfd);
696 free(directive);
697 free(value);
698 errno = x;
699
700 return (rv);
701 }
702
703 /*
704 * Parse the configuration file at `path' and execute the `parse' call-back
705 * functions for any directives defined by the array of config options (first
706 * argument). This is a convenience wrapper around bsdconf_fparse() above.
707 *
708 * Returns zero on success; otherwise returns -1 (or the non-zero result of a
709 * call-back) and errno should be consulted.
710 */
711 int
bsdconf_parse(struct bsdconf_option options[],const char * path,int (* unknown)(struct bsdconf_option * option,uint32_t line,char * directive,char * value),uint16_t processing_options)712 bsdconf_parse(struct bsdconf_option options[], const char *path,
713 int (*unknown)(struct bsdconf_option *option, uint32_t line,
714 char *directive, char *value), uint16_t processing_options)
715 {
716 int error;
717 int fd;
718
719 /* Sanity check: if no options and no unknown function, return */
720 if (path == NULL || (options == NULL && unknown == NULL)) {
721 errno = EINVAL;
722 return (-1);
723 }
724
725 /* Open the file */
726 if ((fd = open(path, O_RDONLY)) < 0)
727 return (-1);
728
729 error = bsdconf_fparse(options, fd, unknown, processing_options);
730
731 close(fd);
732 return (error);
733 }
734