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