xref: /freebsd/lib/libbsdconf/bsdconf_stmt.c (revision f9bc005b8ef3b507a5ecbc2d7fe4411ef70eff38)
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 *
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
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 *
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
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 *
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
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
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
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
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 *
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
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
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
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
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