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