xref: /freebsd/lib/libbsdconf/bsdconf_put.c (revision 90ad63b540317ce7c349b5ebab065b40ba748c27)
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