13fe5961aSDevin Teske /*
23fe5961aSDevin Teske * Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org>
33fe5961aSDevin Teske * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
43fe5961aSDevin Teske *
53fe5961aSDevin Teske * SPDX-License-Identifier: BSD-2-Clause
63fe5961aSDevin Teske */
73fe5961aSDevin Teske
83fe5961aSDevin Teske #include <sys/stat.h>
93fe5961aSDevin Teske
103fe5961aSDevin Teske #include <ctype.h>
113fe5961aSDevin Teske #include <errno.h>
123fe5961aSDevin Teske #include <fcntl.h>
133fe5961aSDevin Teske #include <limits.h>
143fe5961aSDevin Teske #include <stdint.h>
153fe5961aSDevin Teske #include <stdio.h>
163fe5961aSDevin Teske #include <stdlib.h>
173fe5961aSDevin Teske #include <string.h>
183fe5961aSDevin Teske #include <unistd.h>
193fe5961aSDevin Teske
203fe5961aSDevin Teske #include "bsdconf.h"
213fe5961aSDevin Teske #include "bsdconf_internal.h"
223fe5961aSDevin Teske
233fe5961aSDevin Teske /*
243fe5961aSDevin Teske * Rewrite the configuration file at `path', applying the per-directive
253fe5961aSDevin Teske * actions described by the array of config options (first argument). Each
263fe5961aSDevin Teske * option's action is one of:
273fe5961aSDevin Teske *
283fe5961aSDevin Teske * BSDCONF_ACTION_SET_VALUE set the directive to option->value.str,
293fe5961aSDevin Teske * editing it in place if present or appending
303fe5961aSDevin Teske * it to the file if absent;
313fe5961aSDevin Teske * BSDCONF_ACTION_REMOVE delete the directive from the file;
323fe5961aSDevin Teske * BSDCONF_ACTION_CHECK report (via option->result) whether the
333fe5961aSDevin Teske * current value differs from option->value,
343fe5961aSDevin Teske * without modifying the file.
353fe5961aSDevin Teske *
363fe5961aSDevin Teske * Comments, blank lines, statement ordering, and the formatting of untouched
373fe5961aSDevin Teske * statements are preserved. With BSDCONF_OPERATOR_EQUALS, a non-default
383fe5961aSDevin Teske * option->op both matches and rewrites the statement's assignment operator
393fe5961aSDevin Teske * (make.conf(5) `+=' et al.). For each processed option, option->result is
403fe5961aSDevin Teske * set to a bitmask of BSDCONF_DIRECTIVE_FOUND, BSDCONF_VALUE_CHANGED,
413fe5961aSDevin Teske * BSDCONF_DIRECTIVE_ADDED, and BSDCONF_DIRECTIVE_REMOVED, and option->line
423fe5961aSDevin Teske * is set to the line of the first match (if found).
433fe5961aSDevin Teske *
443fe5961aSDevin Teske * When every option is BSDCONF_ACTION_CHECK, or every SET_VALUE/REMOVE would
453fe5961aSDevin Teske * leave the file unchanged, the original is left untouched: no temporary is
463fe5961aSDevin Teske * created, mtime is not bumped, and hard links are not severed. Otherwise
473fe5961aSDevin Teske * the file is replaced atomically: output is written to a temporary file in
483fe5961aSDevin Teske * the same directory, flushed to disk with fsync(2), given the original's
493fe5961aSDevin Teske * mode masked to 0666 (and, if permitted, its ownership), and then renamed
503fe5961aSDevin Teske * over it. A crash mid-transaction leaves the original untouched.
513fe5961aSDevin Teske *
523fe5961aSDevin Teske * Returns zero on success; otherwise returns -1 and errno should be
533fe5961aSDevin Teske * consulted.
543fe5961aSDevin Teske */
553fe5961aSDevin Teske int
bsdconf_put(struct bsdconf_option options[],const char * path,uint16_t processing_options,uint16_t put_options)563fe5961aSDevin Teske bsdconf_put(struct bsdconf_option options[], const char *path,
573fe5961aSDevin Teske uint16_t processing_options, uint16_t put_options)
583fe5961aSDevin Teske {
593fe5961aSDevin Teske bool backup;
603fe5961aSDevin Teske bool bequals;
613fe5961aSDevin Teske bool bsemicolon;
623fe5961aSDevin Teske bool case_sensitive;
633fe5961aSDevin Teske bool emptyok;
643fe5961aSDevin Teske bool nodup;
653fe5961aSDevin Teske bool operator_equals;
663fe5961aSDevin Teske bool quote_always;
673fe5961aSDevin Teske bool require_equals;
683fe5961aSDevin Teske bool strict_equals;
693fe5961aSDevin Teske bool unquoted;
703fe5961aSDevin Teske int dirfd = -1;
713fe5961aSDevin Teske int fd = -1;
723fe5961aSDevin Teske int last_ch = -1; /* last byte emitted, or -1 if none */
733fe5961aSDevin Teske int opchange;
743fe5961aSDevin Teske int rv = -1;
753fe5961aSDevin Teske int saved_errno;
763fe5961aSDevin Teske int tmpfd = -1;
773fe5961aSDevin Teske char *buf = NULL;
783fe5961aSDevin Teske char *slash;
793fe5961aSDevin Teske char *val = NULL;
803fe5961aSDevin Teske const char *sep;
813fe5961aSDevin Teske size_t buflen = 0;
823fe5961aSDevin Teske size_t i;
833fe5961aSDevin Teske size_t n;
843fe5961aSDevin Teske size_t vlen;
853fe5961aSDevin Teske size_t wc; /* write cursor: next unemitted byte of buf */
863fe5961aSDevin Teske uint32_t line = 1;
873fe5961aSDevin Teske struct bsdconf_option *option;
883fe5961aSDevin Teske struct bsdconf_stmt st;
893fe5961aSDevin Teske struct stat sb;
903fe5961aSDevin Teske char rpath[PATH_MAX];
913fe5961aSDevin Teske char tpath[PATH_MAX];
923fe5961aSDevin Teske
933fe5961aSDevin Teske /* Sanity check the arguments */
943fe5961aSDevin Teske if (options == NULL || path == NULL) {
953fe5961aSDevin Teske errno = EINVAL;
963fe5961aSDevin Teske return (-1);
973fe5961aSDevin Teske }
983fe5961aSDevin Teske
993fe5961aSDevin Teske /* Nothing to unlink until mkstemp(3) succeeds (see cleanup) */
1003fe5961aSDevin Teske tpath[0] = '\0';
1013fe5961aSDevin Teske
1023fe5961aSDevin Teske /* Decode processing options */
103*90ad63b5SDevin Teske bequals = processing_options & BSDCONF_BREAK_ON_EQUALS;
104*90ad63b5SDevin Teske bsemicolon = processing_options & BSDCONF_BREAK_ON_SEMICOLON;
105*90ad63b5SDevin Teske case_sensitive = processing_options & BSDCONF_CASE_SENSITIVE;
106*90ad63b5SDevin Teske operator_equals = processing_options & BSDCONF_OPERATOR_EQUALS;
107*90ad63b5SDevin Teske require_equals = processing_options & BSDCONF_REQUIRE_EQUALS;
108*90ad63b5SDevin Teske strict_equals = processing_options & BSDCONF_STRICT_EQUALS;
1093fe5961aSDevin Teske
1103fe5961aSDevin Teske /* Decode put options */
111*90ad63b5SDevin Teske backup = put_options & BSDCONF_PUT_BACKUP;
112*90ad63b5SDevin Teske emptyok = put_options & BSDCONF_PUT_ALLOW_EMPTY;
113*90ad63b5SDevin Teske nodup = put_options & BSDCONF_PUT_NO_DUPLICATES;
114*90ad63b5SDevin Teske quote_always = put_options & BSDCONF_PUT_QUOTE_ALWAYS;
115*90ad63b5SDevin Teske unquoted = put_options & BSDCONF_PUT_UNQUOTED;
1163fe5961aSDevin Teske
1173fe5961aSDevin Teske /* Quoting directives are mutually exclusive */
1183fe5961aSDevin Teske if (unquoted && quote_always) {
1193fe5961aSDevin Teske errno = EINVAL;
1203fe5961aSDevin Teske return (-1);
1213fe5961aSDevin Teske }
1223fe5961aSDevin Teske
1233fe5961aSDevin Teske /* Reset per-option results and reject empty values up front */
1243fe5961aSDevin Teske for (n = 0; options[n].directive != NULL; n++) {
1253fe5961aSDevin Teske options[n].result = 0;
1263fe5961aSDevin Teske options[n].line = 0;
1273fe5961aSDevin Teske if (!emptyok && options[n].action == BSDCONF_ACTION_SET_VALUE &&
1283fe5961aSDevin Teske bsdconf_value_empty(&options[n])) {
1293fe5961aSDevin Teske errno = EINVAL;
1303fe5961aSDevin Teske return (-1);
1313fe5961aSDevin Teske }
1323fe5961aSDevin Teske }
1333fe5961aSDevin Teske
1343fe5961aSDevin Teske /* Resolve the file path */
1353fe5961aSDevin Teske if (realpath(path, rpath) == NULL)
1363fe5961aSDevin Teske return (-1);
1373fe5961aSDevin Teske
1383fe5961aSDevin Teske /*
1393fe5961aSDevin Teske * Open the original for reading. Only a regular file can be
1403fe5961aSDevin Teske * atomically replaced, so anything else is rejected below;
1413fe5961aSDevin Teske * O_NONBLOCK makes that check reachable (it is a no-op for regular
1423fe5961aSDevin Teske * files, but without it opening a fifo blocks awaiting a writer).
1433fe5961aSDevin Teske */
1443fe5961aSDevin Teske if ((fd = open(rpath, O_RDONLY | O_NONBLOCK)) < 0)
1453fe5961aSDevin Teske return (-1);
1463fe5961aSDevin Teske if (fstat(fd, &sb) != 0)
1473fe5961aSDevin Teske goto cleanup;
1483fe5961aSDevin Teske if (!S_ISREG(sb.st_mode)) {
1493fe5961aSDevin Teske errno = EINVAL;
1503fe5961aSDevin Teske goto cleanup;
1513fe5961aSDevin Teske }
1523fe5961aSDevin Teske
1533fe5961aSDevin Teske /* Slurp the original into memory */
1543fe5961aSDevin Teske if ((buf = bsdconf_readfile(fd, (size_t)sb.st_size, &buflen)) == NULL)
1553fe5961aSDevin Teske goto cleanup;
1563fe5961aSDevin Teske
1573fe5961aSDevin Teske /*
1583fe5961aSDevin Teske * Walk the original statement by statement. The replacement temporary
1593fe5961aSDevin Teske * is created lazily on the first real edit (see bsdconf_ensure_tmp());
1603fe5961aSDevin Teske * CHECK and equal-value paths never open it. Bytes are emitted in
1613fe5961aSDevin Teske * order via the write cursor `wc'; a matched statement diverts
1623fe5961aSDevin Teske * around the region it edits or removes.
1633fe5961aSDevin Teske */
1643fe5961aSDevin Teske wc = 0;
1653fe5961aSDevin Teske i = 0;
1663fe5961aSDevin Teske while (bsdconf_scan(buf, buflen, &i, &line, bequals, bsemicolon,
1673fe5961aSDevin Teske strict_equals, operator_equals, &st)) {
1683fe5961aSDevin Teske /* Statements without an `=' are unwritable targets */
1693fe5961aSDevin Teske if (require_equals && !st.have_equals)
1703fe5961aSDevin Teske continue;
1713fe5961aSDevin Teske
1723fe5961aSDevin Teske /* Locate the option (if any) matching this directive */
1733fe5961aSDevin Teske option = NULL;
1743fe5961aSDevin Teske for (n = 0; options[n].directive != NULL; n++) {
1753fe5961aSDevin Teske if (!bsdconf_dir_matches(buf, st.dir_start, st.dir_end,
1763fe5961aSDevin Teske options[n].directive, case_sensitive))
1773fe5961aSDevin Teske continue;
1783fe5961aSDevin Teske /*
1793fe5961aSDevin Teske * A non-zero match_line selects one physical
1803fe5961aSDevin Teske * statement (e.g. make(1) `+=' strike/edit of the
1813fe5961aSDevin Teske * last assignment containing a word).
1823fe5961aSDevin Teske */
1833fe5961aSDevin Teske if (options[n].match_line != 0 &&
1843fe5961aSDevin Teske options[n].match_line != st.line)
1853fe5961aSDevin Teske continue;
1863fe5961aSDevin Teske option = &options[n];
1873fe5961aSDevin Teske break;
1883fe5961aSDevin Teske }
1893fe5961aSDevin Teske if (option == NULL)
1903fe5961aSDevin Teske continue; /* untouched; bytes flushed later */
1913fe5961aSDevin Teske
1923fe5961aSDevin Teske /*
1933fe5961aSDevin Teske * make(1)-style `+=': when match_line is unset, never rewrite
1943fe5961aSDevin Teske * an existing statement and do not mark the directive FOUND,
1953fe5961aSDevin Teske * so a fresh `name+=value' line is appended below. Existing
1963fe5961aSDevin Teske * assignments for the same name are left intact so make's
1973fe5961aSDevin Teske * cumulative semantics are preserved. With match_line set,
1983fe5961aSDevin Teske * rewrite (or remove) that specific statement instead. The
1993fe5961aSDevin Teske * caller (sysconf(8)) skips the put entirely when an
2003fe5961aSDevin Teske * identical `+=value' line is already present.
2013fe5961aSDevin Teske */
2023fe5961aSDevin Teske if (operator_equals &&
2033fe5961aSDevin Teske option->action == BSDCONF_ACTION_SET_VALUE &&
2043fe5961aSDevin Teske option->op == BSDCONF_OP_APPEND &&
2053fe5961aSDevin Teske option->match_line == 0)
2063fe5961aSDevin Teske continue;
2073fe5961aSDevin Teske
2083fe5961aSDevin Teske /* Enforce the no-duplicates policy */
2093fe5961aSDevin Teske if (nodup && (option->result & BSDCONF_DIRECTIVE_FOUND) != 0) {
2103fe5961aSDevin Teske errno = EEXIST;
2113fe5961aSDevin Teske goto cleanup;
2123fe5961aSDevin Teske }
2133fe5961aSDevin Teske if ((option->result & BSDCONF_DIRECTIVE_FOUND) == 0)
2143fe5961aSDevin Teske option->line = st.line;
2153fe5961aSDevin Teske option->result |= BSDCONF_DIRECTIVE_FOUND;
2163fe5961aSDevin Teske
2173fe5961aSDevin Teske if (option->action == BSDCONF_ACTION_REMOVE) {
2183fe5961aSDevin Teske size_t rm_start;
2193fe5961aSDevin Teske size_t rm_end;
2203fe5961aSDevin Teske size_t k;
2213fe5961aSDevin Teske int done = 0;
2223fe5961aSDevin Teske int first_on_line = 1;
2233fe5961aSDevin Teske
2243fe5961aSDevin Teske /*
2253fe5961aSDevin Teske * Is this the first statement on its physical line?
2263fe5961aSDevin Teske * With BSDCONF_BREAK_ON_SEMICOLON an earlier
2273fe5961aSDevin Teske * statement may precede it on the same line.
2283fe5961aSDevin Teske */
2293fe5961aSDevin Teske for (k = st.line_start; k < st.dir_start; k++)
2303fe5961aSDevin Teske if (!isspace((unsigned char)buf[k])) {
2313fe5961aSDevin Teske first_on_line = 0;
2323fe5961aSDevin Teske break;
2333fe5961aSDevin Teske }
2343fe5961aSDevin Teske
2353fe5961aSDevin Teske /*
2363fe5961aSDevin Teske * If a further statement follows on the same line
2373fe5961aSDevin Teske * (terminator is a semicolon with a directive after
2383fe5961aSDevin Teske * it), drop this statement and the trailing
2393fe5961aSDevin Teske * separator, leaving the neighbour intact.
2403fe5961aSDevin Teske */
2413fe5961aSDevin Teske if (bsemicolon && st.term < buflen &&
2423fe5961aSDevin Teske buf[st.term] == ';') {
2433fe5961aSDevin Teske size_t f = st.term + 1;
2443fe5961aSDevin Teske
2453fe5961aSDevin Teske while (f < buflen &&
2463fe5961aSDevin Teske isspace((unsigned char)buf[f]) &&
2473fe5961aSDevin Teske buf[f] != '\n')
2483fe5961aSDevin Teske f++;
2493fe5961aSDevin Teske if (f < buflen && buf[f] != '\n' &&
2503fe5961aSDevin Teske buf[f] != '#') {
2513fe5961aSDevin Teske rm_start = st.dir_start;
2523fe5961aSDevin Teske rm_end = f;
2533fe5961aSDevin Teske done = 1;
2543fe5961aSDevin Teske }
2553fe5961aSDevin Teske }
2563fe5961aSDevin Teske
2573fe5961aSDevin Teske if (!done && first_on_line) {
2583fe5961aSDevin Teske /* Alone on the line: drop the whole line */
2593fe5961aSDevin Teske rm_start = st.line_start;
2603fe5961aSDevin Teske rm_end = st.line_end;
2613fe5961aSDevin Teske } else if (!done) {
2623fe5961aSDevin Teske /*
2633fe5961aSDevin Teske * Last on a shared line: drop the preceding
2643fe5961aSDevin Teske * separator (whitespace, `;', whitespace)
2653fe5961aSDevin Teske * along with this statement, keeping the
2663fe5961aSDevin Teske * terminator.
2673fe5961aSDevin Teske */
2683fe5961aSDevin Teske size_t s = st.dir_start;
2693fe5961aSDevin Teske
2703fe5961aSDevin Teske while (s > st.line_start &&
2713fe5961aSDevin Teske isspace((unsigned char)buf[s - 1]))
2723fe5961aSDevin Teske s--;
2733fe5961aSDevin Teske if (s > st.line_start && buf[s - 1] == ';')
2743fe5961aSDevin Teske s--;
2753fe5961aSDevin Teske while (s > st.line_start &&
2763fe5961aSDevin Teske isspace((unsigned char)buf[s - 1]))
2773fe5961aSDevin Teske s--;
2783fe5961aSDevin Teske rm_start = s;
2793fe5961aSDevin Teske rm_end = st.term;
2803fe5961aSDevin Teske }
2813fe5961aSDevin Teske
2823fe5961aSDevin Teske /* Never rewind before already-emitted bytes */
2833fe5961aSDevin Teske if (rm_start < wc)
2843fe5961aSDevin Teske rm_start = wc;
2853fe5961aSDevin Teske if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
2863fe5961aSDevin Teske rpath, &sb) != 0)
2873fe5961aSDevin Teske goto cleanup;
2883fe5961aSDevin Teske if (bsdconf_emit(tmpfd, buf + wc, rm_start - wc,
2893fe5961aSDevin Teske &last_ch) != 0)
2903fe5961aSDevin Teske goto cleanup;
2913fe5961aSDevin Teske if (rm_end > wc)
2923fe5961aSDevin Teske wc = rm_end;
2933fe5961aSDevin Teske option->result |= BSDCONF_DIRECTIVE_REMOVED;
2943fe5961aSDevin Teske continue;
2953fe5961aSDevin Teske }
2963fe5961aSDevin Teske
2973fe5961aSDevin Teske /* SET_VALUE and CHECK both need the formatted value */
2983fe5961aSDevin Teske free(val);
2993fe5961aSDevin Teske if ((val = bsdconf_format_value(option, unquoted,
3003fe5961aSDevin Teske quote_always, bsemicolon)) == NULL)
3013fe5961aSDevin Teske goto cleanup;
3023fe5961aSDevin Teske vlen = strlen(val);
3033fe5961aSDevin Teske
3043fe5961aSDevin Teske /* Does the assignment operator need to be rewritten? */
3053fe5961aSDevin Teske opchange = operator_equals &&
3063fe5961aSDevin Teske option->op != BSDCONF_OP_DEFAULT && st.have_equals &&
3073fe5961aSDevin Teske st.op != option->op;
3083fe5961aSDevin Teske
3093fe5961aSDevin Teske /* Compare against the value currently in the file */
3103fe5961aSDevin Teske n = st.val_end - st.val_start;
3113fe5961aSDevin Teske if (!opchange && vlen == n && (n == 0 ||
3123fe5961aSDevin Teske memcmp(val, buf + st.val_start, n) == 0)) {
3133fe5961aSDevin Teske /* Unchanged; nothing to do for either action */
3143fe5961aSDevin Teske continue;
3153fe5961aSDevin Teske }
3163fe5961aSDevin Teske
3173fe5961aSDevin Teske if (option->action == BSDCONF_ACTION_CHECK) {
3183fe5961aSDevin Teske option->result |= BSDCONF_VALUE_CHANGED;
3193fe5961aSDevin Teske continue;
3203fe5961aSDevin Teske }
3213fe5961aSDevin Teske
3223fe5961aSDevin Teske /* BSDCONF_ACTION_SET_VALUE and an edit is required */
3233fe5961aSDevin Teske if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
3243fe5961aSDevin Teske rpath, &sb) != 0)
3253fe5961aSDevin Teske goto cleanup;
3263fe5961aSDevin Teske if (opchange) {
3273fe5961aSDevin Teske /* Rewrite the operator token in place */
3283fe5961aSDevin Teske if (bsdconf_emit(tmpfd, buf + wc, st.op_start - wc,
3293fe5961aSDevin Teske &last_ch) != 0)
3303fe5961aSDevin Teske goto cleanup;
3313fe5961aSDevin Teske if (bsdconf_emit(tmpfd, bsdconf_op_token(option->op),
3323fe5961aSDevin Teske strlen(bsdconf_op_token(option->op)),
3333fe5961aSDevin Teske &last_ch) != 0)
3343fe5961aSDevin Teske goto cleanup;
3353fe5961aSDevin Teske wc = st.eq + 1;
3363fe5961aSDevin Teske }
3373fe5961aSDevin Teske
3383fe5961aSDevin Teske if (st.have_value) {
3393fe5961aSDevin Teske /* Replace the existing value in place */
3403fe5961aSDevin Teske if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc,
3413fe5961aSDevin Teske &last_ch) != 0)
3423fe5961aSDevin Teske goto cleanup;
3433fe5961aSDevin Teske if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0)
3443fe5961aSDevin Teske goto cleanup;
3453fe5961aSDevin Teske wc = st.val_end;
3463fe5961aSDevin Teske } else if (option->type != BSDCONF_TYPE_NONE) {
3473fe5961aSDevin Teske /* Insert a value onto a value-less directive */
3483fe5961aSDevin Teske if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc,
3493fe5961aSDevin Teske &last_ch) != 0)
3503fe5961aSDevin Teske goto cleanup;
3513fe5961aSDevin Teske /* Supply a separator unless one already precedes */
3523fe5961aSDevin Teske if (st.val_start == wc ||
3533fe5961aSDevin Teske (buf[st.val_start - 1] != '=' &&
3543fe5961aSDevin Teske !isspace((unsigned char)buf[st.val_start - 1]))) {
3553fe5961aSDevin Teske sep = st.have_equals || require_equals ||
3563fe5961aSDevin Teske bequals ? "=" : " ";
3573fe5961aSDevin Teske if (bsdconf_emit(tmpfd, sep, 1,
3583fe5961aSDevin Teske &last_ch) != 0)
3593fe5961aSDevin Teske goto cleanup;
3603fe5961aSDevin Teske }
3613fe5961aSDevin Teske if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0)
3623fe5961aSDevin Teske goto cleanup;
3633fe5961aSDevin Teske wc = st.val_start;
3643fe5961aSDevin Teske }
3653fe5961aSDevin Teske option->result |= BSDCONF_VALUE_CHANGED;
3663fe5961aSDevin Teske }
3673fe5961aSDevin Teske
3683fe5961aSDevin Teske /*
3693fe5961aSDevin Teske * Append any SET_VALUE directives that were not found, and finalize
3703fe5961aSDevin Teske * the result flags for CHECK directives that were absent. The tail of
3713fe5961aSDevin Teske * the original is flushed only when a temporary already exists or an
3723fe5961aSDevin Teske * append forces one into being.
3733fe5961aSDevin Teske */
3743fe5961aSDevin Teske for (n = 0; options[n].directive != NULL; n++) {
3753fe5961aSDevin Teske option = &options[n];
3763fe5961aSDevin Teske if ((option->result & BSDCONF_DIRECTIVE_FOUND) != 0)
3773fe5961aSDevin Teske continue;
3783fe5961aSDevin Teske if (option->action == BSDCONF_ACTION_CHECK) {
3793fe5961aSDevin Teske /* Absent means it differs from the desired value */
3803fe5961aSDevin Teske option->result |= BSDCONF_VALUE_CHANGED;
3813fe5961aSDevin Teske continue;
3823fe5961aSDevin Teske }
3833fe5961aSDevin Teske if (option->action != BSDCONF_ACTION_SET_VALUE)
3843fe5961aSDevin Teske continue; /* REMOVE of an absent directive: no-op */
3853fe5961aSDevin Teske
3863fe5961aSDevin Teske if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
3873fe5961aSDevin Teske rpath, &sb) != 0)
3883fe5961aSDevin Teske goto cleanup;
3893fe5961aSDevin Teske /* Flush any unread prefix once before the first append */
3903fe5961aSDevin Teske if (wc < buflen) {
3913fe5961aSDevin Teske if (bsdconf_emit(tmpfd, buf + wc, buflen - wc,
3923fe5961aSDevin Teske &last_ch) != 0)
3933fe5961aSDevin Teske goto cleanup;
3943fe5961aSDevin Teske wc = buflen;
3953fe5961aSDevin Teske }
3963fe5961aSDevin Teske
3973fe5961aSDevin Teske /* Ensure the emitted output ends with a newline first */
3983fe5961aSDevin Teske if (last_ch != -1 && last_ch != '\n') {
3993fe5961aSDevin Teske if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0)
4003fe5961aSDevin Teske goto cleanup;
4013fe5961aSDevin Teske }
4023fe5961aSDevin Teske
4033fe5961aSDevin Teske if (bsdconf_emit(tmpfd, option->directive,
4043fe5961aSDevin Teske strlen(option->directive), &last_ch) != 0)
4053fe5961aSDevin Teske goto cleanup;
4063fe5961aSDevin Teske if (option->type != BSDCONF_TYPE_NONE) {
4073fe5961aSDevin Teske free(val);
4083fe5961aSDevin Teske if ((val = bsdconf_format_value(option, unquoted,
4093fe5961aSDevin Teske quote_always, bsemicolon)) == NULL)
4103fe5961aSDevin Teske goto cleanup;
4113fe5961aSDevin Teske if (operator_equals &&
4123fe5961aSDevin Teske option->op != BSDCONF_OP_DEFAULT)
4133fe5961aSDevin Teske sep = bsdconf_op_token(option->op);
4143fe5961aSDevin Teske else
4153fe5961aSDevin Teske sep = (bequals || require_equals) ? "=" : " ";
4163fe5961aSDevin Teske if (bsdconf_emit(tmpfd, sep, strlen(sep),
4173fe5961aSDevin Teske &last_ch) != 0)
4183fe5961aSDevin Teske goto cleanup;
4193fe5961aSDevin Teske if (bsdconf_emit(tmpfd, val, strlen(val),
4203fe5961aSDevin Teske &last_ch) != 0)
4213fe5961aSDevin Teske goto cleanup;
4223fe5961aSDevin Teske }
4233fe5961aSDevin Teske if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0)
4243fe5961aSDevin Teske goto cleanup;
4253fe5961aSDevin Teske /*
4263fe5961aSDevin Teske * Line number of the statement appended at EOF: one past
4273fe5961aSDevin Teske * the number of newlines in the original, or the next line
4283fe5961aSDevin Teske * if we have to emit a separating newline first.
4293fe5961aSDevin Teske */
4303fe5961aSDevin Teske {
4313fe5961aSDevin Teske uint32_t al = 1;
4323fe5961aSDevin Teske size_t k;
4333fe5961aSDevin Teske
4343fe5961aSDevin Teske for (k = 0; k < buflen; k++)
4353fe5961aSDevin Teske if (buf[k] == '\n')
4363fe5961aSDevin Teske al++;
4373fe5961aSDevin Teske if (buflen > 0 && buf[buflen - 1] != '\n')
4383fe5961aSDevin Teske al++;
4393fe5961aSDevin Teske option->line = al;
4403fe5961aSDevin Teske }
4413fe5961aSDevin Teske option->result |=
4423fe5961aSDevin Teske BSDCONF_DIRECTIVE_ADDED | BSDCONF_VALUE_CHANGED;
4433fe5961aSDevin Teske }
4443fe5961aSDevin Teske
4453fe5961aSDevin Teske /*
4463fe5961aSDevin Teske * Nothing changed: every option was CHECK or an equal-value
4473fe5961aSDevin Teske * SET_VALUE/REMOVE-miss. Leave the original file alone.
4483fe5961aSDevin Teske */
4493fe5961aSDevin Teske if (tmpfd < 0) {
4503fe5961aSDevin Teske rv = 0;
4513fe5961aSDevin Teske goto cleanup;
4523fe5961aSDevin Teske }
4533fe5961aSDevin Teske
4543fe5961aSDevin Teske /* Flush any remaining unread tail of the original */
4553fe5961aSDevin Teske if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, &last_ch) != 0)
4563fe5961aSDevin Teske goto cleanup;
4573fe5961aSDevin Teske
4583fe5961aSDevin Teske /* Optionally back up the original before replacing it */
4593fe5961aSDevin Teske if (backup) {
4603fe5961aSDevin Teske char bpath[PATH_MAX];
4613fe5961aSDevin Teske int bfd;
4623fe5961aSDevin Teske
4633fe5961aSDevin Teske if (snprintf(bpath, sizeof(bpath), "%s.bak", rpath) >=
4643fe5961aSDevin Teske (int)sizeof(bpath)) {
4653fe5961aSDevin Teske errno = ENAMETOOLONG;
4663fe5961aSDevin Teske goto cleanup;
4673fe5961aSDevin Teske }
4683fe5961aSDevin Teske /*
4693fe5961aSDevin Teske * O_NOFOLLOW: refuse to follow a planted symbolic link
4703fe5961aSDevin Teske * lest the backup clobber whatever it points at.
4713fe5961aSDevin Teske */
4723fe5961aSDevin Teske if ((bfd = open(bpath, O_WRONLY | O_CREAT | O_TRUNC |
4733fe5961aSDevin Teske O_NOFOLLOW, 0600)) < 0)
4743fe5961aSDevin Teske goto cleanup;
4753fe5961aSDevin Teske if (fchmod(bfd, sb.st_mode & 0777) != 0) {
4763fe5961aSDevin Teske saved_errno = errno;
4773fe5961aSDevin Teske close(bfd);
4783fe5961aSDevin Teske errno = saved_errno;
4793fe5961aSDevin Teske goto cleanup;
4803fe5961aSDevin Teske }
4813fe5961aSDevin Teske if (bsdconf_writeall(bfd, buf, buflen) != 0) {
4823fe5961aSDevin Teske saved_errno = errno;
4833fe5961aSDevin Teske close(bfd);
4843fe5961aSDevin Teske errno = saved_errno;
4853fe5961aSDevin Teske goto cleanup;
4863fe5961aSDevin Teske }
4873fe5961aSDevin Teske if (close(bfd) != 0)
4883fe5961aSDevin Teske goto cleanup;
4893fe5961aSDevin Teske }
4903fe5961aSDevin Teske
4913fe5961aSDevin Teske /* Commit: flush to disk, then atomically replace the original */
4923fe5961aSDevin Teske if (fsync(tmpfd) != 0)
4933fe5961aSDevin Teske goto cleanup;
4943fe5961aSDevin Teske if (close(tmpfd) != 0) {
4953fe5961aSDevin Teske tmpfd = -1;
4963fe5961aSDevin Teske goto cleanup;
4973fe5961aSDevin Teske }
4983fe5961aSDevin Teske tmpfd = -1;
4993fe5961aSDevin Teske if (rename(tpath, rpath) != 0)
5003fe5961aSDevin Teske goto cleanup;
5013fe5961aSDevin Teske tpath[0] = '\0'; /* renamed away; nothing to unlink */
5023fe5961aSDevin Teske
5033fe5961aSDevin Teske /* Best-effort: persist the directory entry change */
5043fe5961aSDevin Teske if ((slash = strrchr(rpath, '/')) != NULL) {
5053fe5961aSDevin Teske char dpath[PATH_MAX];
5063fe5961aSDevin Teske size_t dlen = (size_t)(slash - rpath);
5073fe5961aSDevin Teske
5083fe5961aSDevin Teske if (dlen == 0)
5093fe5961aSDevin Teske dlen = 1; /* the root directory */
5103fe5961aSDevin Teske if (dlen < sizeof(dpath)) {
5113fe5961aSDevin Teske memcpy(dpath, rpath, dlen);
5123fe5961aSDevin Teske dpath[dlen] = '\0';
5133fe5961aSDevin Teske if ((dirfd = open(dpath, O_RDONLY)) >= 0) {
5143fe5961aSDevin Teske (void)fsync(dirfd);
5153fe5961aSDevin Teske close(dirfd);
5163fe5961aSDevin Teske dirfd = -1;
5173fe5961aSDevin Teske }
5183fe5961aSDevin Teske }
5193fe5961aSDevin Teske }
5203fe5961aSDevin Teske
5213fe5961aSDevin Teske rv = 0;
5223fe5961aSDevin Teske
5233fe5961aSDevin Teske cleanup:
5243fe5961aSDevin Teske saved_errno = errno;
5253fe5961aSDevin Teske if (fd >= 0)
5263fe5961aSDevin Teske close(fd);
5273fe5961aSDevin Teske if (tmpfd >= 0)
5283fe5961aSDevin Teske close(tmpfd);
5293fe5961aSDevin Teske if (dirfd >= 0)
5303fe5961aSDevin Teske close(dirfd);
5313fe5961aSDevin Teske if (rv != 0 && tpath[0] != '\0')
5323fe5961aSDevin Teske (void)unlink(tpath); /* discard the incomplete temporary */
5333fe5961aSDevin Teske free(buf);
5343fe5961aSDevin Teske free(val);
5353fe5961aSDevin Teske errno = saved_errno;
5363fe5961aSDevin Teske return (rv);
5373fe5961aSDevin Teske }
538