1 /*
2 * Copyright (c) 2015-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 /*
9 * Statement scanning, value rendering, and related helpers used by
10 * bsdconf_put(). Kept separate so bsdconf_put.c stays focused on the
11 * rewrite driver.
12 */
13
14 #include <sys/stat.h>
15
16 #include <ctype.h>
17 #include <errno.h>
18 #include <stdint.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #include <strings.h>
23 #include <unistd.h>
24
25 #include "bsdconf.h"
26 #include "bsdconf_internal.h"
27
28 /*
29 * Map an assignment operator to its config file token. BSDCONF_OP_DEFAULT
30 * maps to the plain `=' token.
31 */
32 const char *
bsdconf_op_token(enum bsdconf_op op)33 bsdconf_op_token(enum bsdconf_op op)
34 {
35 switch (op) {
36 case BSDCONF_OP_APPEND:
37 return ("+=");
38 case BSDCONF_OP_COND:
39 return ("?=");
40 case BSDCONF_OP_EXPAND:
41 return (":=");
42 case BSDCONF_OP_SHELL:
43 return ("!=");
44 case BSDCONF_OP_DEFAULT:
45 case BSDCONF_OP_ASSIGN:
46 default:
47 return ("=");
48 }
49 }
50
51 /*
52 * Write exactly `len' bytes from `data' to `fd', restarting short and
53 * interrupted writes. Returns zero on success; -1 (with errno set) on error.
54 * Shared with bsdconf_spool() (see bsdconf_internal.h).
55 */
56 int
bsdconf_writeall(int fd,const void * data,size_t len)57 bsdconf_writeall(int fd, const void *data, size_t len)
58 {
59 const char *p = data;
60 ssize_t w;
61
62 while (len > 0) {
63 w = write(fd, p, len);
64 if (w < 0) {
65 if (errno == EINTR)
66 continue;
67 return (-1);
68 }
69 if (w == 0) {
70 errno = EIO;
71 return (-1);
72 }
73 p += w;
74 len -= (size_t)w;
75 }
76 return (0);
77 }
78
79 /*
80 * Read the entire contents of the open descriptor `fd' (whose size is `size')
81 * into a freshly allocated, NUL-terminated buffer. The actual number of bytes
82 * read is stored through `lenp'. Returns the buffer on success (which the
83 * caller must free) or NULL (with errno set) on error.
84 */
85 char *
bsdconf_readfile(int fd,size_t size,size_t * lenp)86 bsdconf_readfile(int fd, size_t size, size_t *lenp)
87 {
88 char *buf;
89 size_t off = 0;
90 ssize_t r;
91
92 if ((buf = malloc(size + 1)) == NULL)
93 return (NULL);
94
95 while (off < size) {
96 r = read(fd, buf + off, size - off);
97 if (r < 0) {
98 if (errno == EINTR)
99 continue;
100 free(buf);
101 return (NULL);
102 }
103 if (r == 0) /* premature EOF (file shrank); stop */
104 break;
105 off += (size_t)r;
106 }
107
108 buf[off] = '\0';
109 *lenp = off;
110 return (buf);
111 }
112
113 /*
114 * Ceiling on a slurp of configuration data. Unset, empty, zero, or
115 * unparseable BSDCONF_MAX_BYTES restores BSDCONF_MAX_BYTES_DEFAULT.
116 */
117 size_t
bsdconf_max_bytes(void)118 bsdconf_max_bytes(void)
119 {
120 const char *s;
121 char *end;
122 unsigned long n;
123
124 s = getenv("BSDCONF_MAX_BYTES");
125 if (s == NULL || *s == '\0')
126 return (BSDCONF_MAX_BYTES_DEFAULT);
127 errno = 0;
128 n = strtoul(s, &end, 10);
129 if (errno != 0 || end == s || *end != '\0' || n == 0)
130 return (BSDCONF_MAX_BYTES_DEFAULT);
131 if (n > SIZE_MAX)
132 return (SIZE_MAX);
133 return ((size_t)n);
134 }
135
136 /*
137 * Read the remaining contents of `fd' into a freshly allocated,
138 * NUL-terminated buffer, stopping at EOF or the BSDCONF_MAX_BYTES cap.
139 * Regular files whose remaining length already exceeds the cap fail with
140 * EFBIG without reading. Returns the buffer on success (which the caller
141 * must free) or NULL (with errno set) on error.
142 */
143 char *
bsdconf_slurp(int fd,size_t * lenp)144 bsdconf_slurp(int fd, size_t *lenp)
145 {
146 struct stat sb;
147 char *buf;
148 char *t;
149 int sverrno;
150 size_t cap;
151 size_t maxb;
152 size_t off;
153 ssize_t r;
154
155 maxb = bsdconf_max_bytes();
156 if (maxb == 0) {
157 errno = EFBIG;
158 return (NULL);
159 }
160
161 /*
162 * Known remaining length on a regular file: fail fast if the cap
163 * cannot admit it, otherwise one exact allocation.
164 */
165 if (fstat(fd, &sb) == 0 && S_ISREG(sb.st_mode)) {
166 off_t cur;
167
168 cur = lseek(fd, 0, SEEK_CUR);
169 if (cur != -1 && sb.st_size >= cur) {
170 uintmax_t remain;
171
172 remain = (uintmax_t)(sb.st_size - cur);
173 if (remain > maxb) {
174 errno = EFBIG;
175 return (NULL);
176 }
177 return (bsdconf_readfile(fd, (size_t)remain, lenp));
178 }
179 }
180
181 /* Unknown length (pipe, socket, device): grow up to the cap */
182 cap = maxb < 8192 ? maxb : 8192;
183 if ((buf = malloc(cap + 1)) == NULL)
184 return (NULL);
185 off = 0;
186 for (;;) {
187 if (off == cap) {
188 size_t ncap;
189
190 if (cap >= maxb) {
191 char probe;
192
193 do {
194 r = read(fd, &probe, 1);
195 } while (r < 0 && errno == EINTR);
196 if (r < 0) {
197 sverrno = errno;
198 free(buf);
199 errno = sverrno;
200 return (NULL);
201 }
202 if (r > 0) {
203 free(buf);
204 errno = EFBIG;
205 return (NULL);
206 }
207 break;
208 }
209 ncap = cap * 2;
210 if (ncap <= cap || ncap > maxb)
211 ncap = maxb;
212 if (ncap <= cap) {
213 free(buf);
214 errno = EFBIG;
215 return (NULL);
216 }
217 if ((t = realloc(buf, ncap + 1)) == NULL) {
218 sverrno = errno;
219 free(buf);
220 errno = sverrno;
221 return (NULL);
222 }
223 buf = t;
224 cap = ncap;
225 }
226 r = read(fd, buf + off, cap - off);
227 if (r < 0) {
228 if (errno == EINTR)
229 continue;
230 sverrno = errno;
231 free(buf);
232 errno = sverrno;
233 return (NULL);
234 }
235 if (r == 0)
236 break;
237 off += (size_t)r;
238 }
239
240 buf[off] = '\0';
241 *lenp = off;
242 return (buf);
243 }
244
245 /*
246 * Write `len' bytes to `fd' and, when any bytes are written, remember the
247 * last one through `last' (as an unsigned char, or left untouched for a
248 * zero-length write). This lets the caller track whether the output so far
249 * ends in a newline without having to re-read the descriptor. Returns zero
250 * on success; -1 (with errno set) on error.
251 */
252 int
bsdconf_emit(int fd,const void * data,size_t len,int * last)253 bsdconf_emit(int fd, const void *data, size_t len, int *last)
254 {
255
256 if (len == 0)
257 return (0);
258 if (bsdconf_writeall(fd, data, len) != 0)
259 return (-1);
260 *last = (unsigned char)((const char *)data)[len - 1];
261 return (0);
262 }
263
264 /*
265 * Lazily create the replacement temporary in the target's directory. Called
266 * on the first edit so that CHECK-only and equal-value SET_VALUE paths never
267 * open a writable descriptor (and never bump mtime or sever hard links).
268 */
269 int
bsdconf_ensure_tmp(int * tmpfdp,char * tpath,size_t tpathsz,const char * rpath,const struct stat * sb)270 bsdconf_ensure_tmp(int *tmpfdp, char *tpath, size_t tpathsz,
271 const char *rpath, const struct stat *sb)
272 {
273 char *slash;
274
275 if (*tmpfdp >= 0)
276 return (0);
277
278 if ((slash = strrchr(rpath, '/')) != NULL) {
279 if (snprintf(tpath, tpathsz, "%.*s/.bsdconf.XXXXXXXXXX",
280 (int)(slash - rpath), rpath) >= (int)tpathsz) {
281 errno = ENAMETOOLONG;
282 return (-1);
283 }
284 } else if (snprintf(tpath, tpathsz, ".bsdconf.XXXXXXXXXX") >=
285 (int)tpathsz) {
286 errno = ENAMETOOLONG;
287 return (-1);
288 }
289 if ((*tmpfdp = mkstemp(tpath)) == -1) {
290 tpath[0] = '\0';
291 return (-1);
292 }
293 if (fchmod(*tmpfdp, sb->st_mode & 0666) != 0)
294 return (-1);
295 (void)fchown(*tmpfdp, sb->st_uid, sb->st_gid); /* best effort */
296 return (0);
297 }
298
299 /*
300 * Determine whether the string value `s' must be double-quoted on output so
301 * that it round-trips through the parser unchanged. Empty strings are quoted
302 * so that they remain distinguishable from a value-less directive.
303 */
304 static int
bsdconf_needs_quote(const char * s,bool bsemicolon)305 bsdconf_needs_quote(const char *s, bool bsemicolon)
306 {
307 const char *p;
308
309 if (*s == '\0')
310 return (1);
311 for (p = s; *p != '\0'; p++) {
312 if (isspace((unsigned char)*p) || *p == '#' || *p == '"' ||
313 *p == '\\')
314 return (1);
315 if (bsemicolon && *p == ';')
316 return (1);
317 }
318 return (0);
319 }
320
321 /*
322 * Determine whether the string value `s' round-trips through the parser
323 * when written verbatim (no quoting available; make.conf(5) semantics).
324 * Embedded whitespace is fine; what is not is an embedded newline, a `#'
325 * (or, with `bsemicolon', a `;') at an unquoted position -- either would
326 * terminate the value early on re-parse -- or a trailing unescaped
327 * backslash, which would escape the statement's own newline.
328 *
329 * After counting a run of backslashes, `p' already points at the following
330 * byte. An odd-length run escapes that byte (skip it); an even-length run
331 * leaves it unescaped, so it must be re-examined by the loop body below
332 * (do not `continue', which would advance past it a second time).
333 */
334 static int
bsdconf_verbatim_ok(const char * s,bool bsemicolon)335 bsdconf_verbatim_ok(const char *s, bool bsemicolon)
336 {
337 const char *p;
338 int quote = 0;
339 size_t nbs;
340
341 for (p = s; *p != '\0'; p++) {
342 if (*p == '\\') {
343 for (nbs = 0; *p == '\\'; p++)
344 nbs++;
345 if (*p == '\0')
346 return ((nbs & 1) == 0);
347 if ((nbs & 1) != 0) {
348 /* Odd run: the following byte is escaped */
349 if (*p == '\n')
350 return (0);
351 continue;
352 }
353 /* Even run: *p is unescaped; fall through */
354 }
355 if (*p == '\n')
356 return (0);
357 if (*p == '"')
358 quote = !quote;
359 else if (!quote &&
360 (*p == '#' || (bsemicolon && *p == ';')))
361 return (0);
362 }
363 return (1);
364 }
365
366 /*
367 * Produce the textual value to be written for `option', following the value
368 * model documented in bsdconf.h: the value is always taken from value.str and
369 * the type governs only whether quoting/escaping is applied.
370 *
371 * With `quote_always' (loader.conf(5) semantics) every value is enclosed in
372 * double-quotes with embedded quotes and backslashes escaped. With `unquoted'
373 * (make.conf(5) semantics / BSDCONF_PUT_UNQUOTED) `value.str' is emitted as
374 * literal file text -- no quotes are added and no characters are
375 * backslash-escaped -- so the caller supplies the bytes that should appear
376 * on disk. Embedded whitespace is fine (the value runs to the end of the
377 * line); a value that could not round-trip under the parser (embedded
378 * newline, unescaped comment marker, or trailing unescaped backslash) is
379 * rejected (errno = EINVAL) rather than silently corrupting the file. Note
380 * the asymmetry with the reader, which still runs bsdconf_strunexpand() on
381 * input: escape sequences present in the file become the logical value on
382 * read, but this path does not re-encode them. Otherwise (sysctl.conf(5)
383 * and generic semantics), values are quoted only when required.
384 *
385 * Returns a newly allocated NUL-terminated string (which the caller must
386 * free) or NULL (with errno set) on failure.
387 */
388 char *
bsdconf_format_value(const struct bsdconf_option * option,bool unquoted,bool quote_always,bool bsemicolon)389 bsdconf_format_value(const struct bsdconf_option *option, bool unquoted,
390 bool quote_always, bool bsemicolon)
391 {
392 const char *s;
393 char *out;
394 char *q;
395 size_t extra;
396 size_t i;
397 size_t slen;
398
399 if (option->type == BSDCONF_TYPE_NONE)
400 return (strdup(""));
401
402 s = option->value.str != NULL ? option->value.str : "";
403
404 if (unquoted) {
405 if (!bsdconf_verbatim_ok(s, bsemicolon)) {
406 errno = EINVAL;
407 return (NULL);
408 }
409 return (strdup(s));
410 }
411
412 if (!quote_always) {
413 if (option->type == BSDCONF_TYPE_INT ||
414 option->type == BSDCONF_TYPE_UINT ||
415 option->type == BSDCONF_TYPE_INT64 ||
416 option->type == BSDCONF_TYPE_UINT64 ||
417 option->type == BSDCONF_TYPE_BOOL)
418 return (strdup(s));
419 if (!bsdconf_needs_quote(s, bsemicolon))
420 return (strdup(s));
421 }
422
423 /* Count the characters that require a backslash escape */
424 slen = strlen(s);
425 extra = 0;
426 for (i = 0; i < slen; i++)
427 if (s[i] == '"' || s[i] == '\\')
428 extra++;
429
430 /* Enclosing quotes plus escapes plus terminator */
431 if ((out = malloc(slen + extra + 3)) == NULL)
432 return (NULL);
433 q = out;
434 *q++ = '"';
435 for (i = 0; i < slen; i++) {
436 if (s[i] == '"' || s[i] == '\\')
437 *q++ = '\\';
438 *q++ = s[i];
439 }
440 *q++ = '"';
441 *q = '\0';
442 return (out);
443 }
444
445 /*
446 * Test whether a SET_VALUE/CHECK value is considered "empty" for the
447 * purposes of BSDCONF_PUT_ALLOW_EMPTY.
448 */
449 int
bsdconf_value_empty(const struct bsdconf_option * option)450 bsdconf_value_empty(const struct bsdconf_option *option)
451 {
452
453 return (option->type != BSDCONF_TYPE_NONE &&
454 (option->value.str == NULL || option->value.str[0] == '\0'));
455 }
456
457 /*
458 * Match the directive spanning buf[start, end) against the directive of a
459 * bsdconf_option. Comparison is exact (whole-token); unlike bsdconf_parse(),
460 * bsdconf_put() does not glob-match, since a pattern is not a writable
461 * target.
462 */
463 int
bsdconf_dir_matches(const char * buf,size_t start,size_t end,const char * directive,bool case_sensitive)464 bsdconf_dir_matches(const char *buf, size_t start, size_t end,
465 const char *directive, bool case_sensitive)
466 {
467 size_t len = end - start;
468
469 if (strlen(directive) != len)
470 return (0);
471 if (case_sensitive)
472 return (memcmp(buf + start, directive, len) == 0);
473 return (strncasecmp(buf + start, directive, len) == 0);
474 }
475
476 /*
477 * Tokenize the statement beginning at buf[*ip], advancing *ip past it and
478 * filling in `st'. The scan mirrors bsdconf_fparse() exactly (comment,
479 * quote, operator, and backslash-escape handling included) so that a file
480 * written by bsdconf_put() re-parses identically. Returns 1 if a statement
481 * was found, or 0 at end of input.
482 */
483 int
bsdconf_scan(const char * buf,size_t len,size_t * ip,uint32_t * linep,bool bequals,bool bsemicolon,bool strict_equals,bool operator_equals,struct bsdconf_stmt * st)484 bsdconf_scan(const char *buf, size_t len, size_t *ip, uint32_t *linep,
485 bool bequals, bool bsemicolon, bool strict_equals, bool operator_equals,
486 struct bsdconf_stmt *st)
487 {
488 size_t i = *ip;
489 size_t j;
490 uint32_t line = *linep;
491 bool comment = false;
492 bool novalue = false;
493 bool quote = false;
494
495 /* Skip whitespace and comments to the beginning of a directive */
496 st->line_start = i;
497 while (i < len && (isspace((unsigned char)buf[i]) || buf[i] == '#' ||
498 comment || (bsemicolon && buf[i] == ';'))) {
499 if (buf[i] == '#')
500 comment = true;
501 else if (buf[i] == '\n') {
502 comment = false;
503 line++;
504 st->line_start = i + 1;
505 }
506 i++;
507 }
508 if (i >= len) {
509 *ip = i;
510 *linep = line;
511 return (0);
512 }
513
514 st->line = line;
515 st->dir_start = i;
516 st->have_equals = false;
517 st->op = BSDCONF_OP_DEFAULT;
518 st->eq = 0;
519
520 /* Find the end of the directive */
521 while (i < len) {
522 if (isspace((unsigned char)buf[i]))
523 break;
524 if (bequals && buf[i] == '=') {
525 st->have_equals = true;
526 break;
527 }
528 if (bsemicolon && buf[i] == ';')
529 break;
530 i++;
531 }
532 st->dir_end = i;
533 st->op_start = i;
534
535 /*
536 * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off the tail
537 * of the directive if requested; the operator character rode along
538 * with the directive because only the `=' terminates the scan.
539 */
540 if (st->have_equals) {
541 st->eq = i;
542 st->op = BSDCONF_OP_ASSIGN;
543 if (operator_equals && st->dir_end - st->dir_start > 1) {
544 switch (buf[st->dir_end - 1]) {
545 case '+': st->op = BSDCONF_OP_APPEND; break;
546 case '?': st->op = BSDCONF_OP_COND; break;
547 case ':': st->op = BSDCONF_OP_EXPAND; break;
548 case '!': st->op = BSDCONF_OP_SHELL; break;
549 }
550 if (st->op != BSDCONF_OP_ASSIGN) {
551 st->dir_end--;
552 st->op_start = st->dir_end;
553 }
554 }
555 }
556
557 /* Step over an `=' acting as the directive terminator */
558 if (bequals && i < len && buf[i] == '=') {
559 i++;
560 if (strict_equals && i < len &&
561 isspace((unsigned char)buf[i]) && buf[i] != '\n')
562 novalue = true; /* strict `=' then space: no value */
563 }
564
565 /* Advance across separating whitespace to the value */
566 if (!novalue && !(bsemicolon && i < len && buf[i] == ';') &&
567 !(strict_equals && i < len && buf[i] == '=')) {
568 while (i < len && isspace((unsigned char)buf[i]) &&
569 buf[i] != '\n')
570 i++;
571 }
572
573 /* Consume an `=' surrounded by whitespace (non-strict) */
574 if (!novalue && i < len && bequals && buf[i] == '=' &&
575 !strict_equals) {
576 st->have_equals = true;
577 st->eq = i;
578 if (st->op == BSDCONF_OP_DEFAULT)
579 st->op = BSDCONF_OP_ASSIGN;
580 i++;
581 while (i < len && isspace((unsigned char)buf[i]) &&
582 buf[i] != '\n')
583 i++;
584 }
585
586 /* A directive with no value */
587 if (novalue || i >= len || buf[i] == '\n' || buf[i] == '#' ||
588 (bsemicolon && buf[i] == ';')) {
589 st->have_value = false;
590 st->val_start = st->val_end = i;
591 } else {
592 st->have_value = true;
593 st->val_start = i;
594 quote = false;
595 while (i < len) {
596 char c = buf[i];
597
598 if (c != '"' && c != '#' && c != '\n' &&
599 (!bsemicolon || c != ';')) {
600 i++;
601 continue;
602 }
603
604 /* Count the backslashes immediately preceding `c' */
605 j = i;
606 while (j > st->val_start && buf[j - 1] == '\\')
607 j--;
608
609 if (((i - j) & 1) == 0) { /* not escaped */
610 if (c == '"') {
611 quote = !quote;
612 i++;
613 continue;
614 }
615 if (c == '#') {
616 if (!quote)
617 break;
618 i++;
619 continue;
620 }
621 if (c == '\n')
622 break;
623 if (c == ';') {
624 if (!quote && bsemicolon)
625 break;
626 i++;
627 continue;
628 }
629 } else { /* escaped: part of the value */
630 if (c == '\n')
631 line++;
632 i++;
633 continue;
634 }
635 }
636 /* Trim trailing whitespace from the value */
637 st->val_end = i;
638 while (st->val_end > st->val_start &&
639 isspace((unsigned char)buf[st->val_end - 1]))
640 st->val_end--;
641 }
642
643 /* Record the terminator position and the end of the physical line */
644 st->term = i;
645 st->line_end = i;
646 while (st->line_end < len && buf[st->line_end] != '\n')
647 st->line_end++;
648 if (st->line_end < len)
649 st->line_end++;
650
651 *ip = i;
652 *linep = line;
653 return (1);
654 }
655
656 /*
657 * Search for a config option (struct bsdconf_option) in the array of config
658 * options and stage the value of the struct whose directive matches the
659 * given parameter. On success, returns 1, otherwise 0.
660 */
661 int
bsdconf_set_option(struct bsdconf_option options[],const char * directive,union bsdconf_value * value)662 bsdconf_set_option(struct bsdconf_option options[], const char *directive,
663 union bsdconf_value *value)
664 {
665 uint32_t n;
666
667 /* Check arguments */
668 if (options == NULL || directive == NULL || value == NULL)
669 return (0);
670
671 /* Loop through the array, staging the first match */
672 for (n = 0; options[n].directive != NULL; n++) {
673 if (strcmp(options[n].directive, directive) == 0) {
674 options[n].value = *value;
675 return (1);
676 }
677 }
678
679 return (0);
680 }
681