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