xref: /freebsd/usr.sbin/sysconf/sysconf_edit_write.c (revision 3fe5961a0b708da599d42cbb6b5e4f030c28ea45)
1 /*
2  * Copyright (c) 2013-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  * Apply or check assignments against conf files (bsdconf_put path).
10  */
11 
12 #include <ctype.h>
13 
14 #include "sysconf_priv.h"
15 
16 /*
17  * Process `-c' from the results of a completed scan_pass() without touching
18  * any file: a write request differs when the authoritative value does not
19  * match, a make(1) `+=value' request differs when no identical `+=value'
20  * line is already present, and a removal request differs when the
21  * directive is still present. Returns EXIT_SUCCESS when no changes would
22  * be required.
23  */
24 int
do_checks(void)25 do_checks(void)
26 {
27 	int differs;
28 	int rv = EXIT_SUCCESS;
29 	unsigned int n;
30 
31 	for (n = 0; n < nreqs; n++) {
32 		if (reqs[n].newvalue == NULL && !reqs[n].remove)
33 			continue;
34 		if (reqs[n].remove)
35 			differs = reqs[n].found;
36 		else if (reqs[n].op == BSDCONF_OP_APPEND)
37 			differs = !reqs[n].append_present;
38 		else
39 			differs = !reqs[n].found ||
40 			    strcmp(reqs[n].value, reqs[n].newvalue) != 0;
41 		if (differs) {
42 			if (!quiet)
43 				warnx("%s: value differs", reqs[n].name);
44 			rv = EXIT_FAILURE;
45 		}
46 	}
47 
48 	return (rv);
49 }
50 
51 /*
52  * Process write and remove requests using the srcidx/infile maps recorded
53  * by scan_pass(). A directive is rewritten in the file that authoritatively
54  * defines it (the last file in sourcing order that lists it) so the change
55  * survives a reboot; a directive defined nowhere is appended to the
56  * format's default file. Removals are applied to every file listing the
57  * directive, lest an earlier definition be unmasked. Each modified file is
58  * replaced in one atomic bsdconf_put() transaction. Returns EXIT_SUCCESS
59  * or EXIT_FAILURE.
60  */
61 int
do_writes(void)62 do_writes(void)
63 {
64 	int fd;
65 	int rv = EXIT_SUCCESS;
66 	size_t f;
67 	unsigned int n;
68 	unsigned int nopts;
69 	struct bsdconf_option *opt;
70 	struct bsdconf_option *options;
71 	struct request **owner;
72 
73 	if ((options = calloc(nreqs + 1, sizeof(*options))) == NULL ||
74 	    (owner = calloc(nreqs, sizeof(*owner))) == NULL)
75 		err(EXIT_FAILURE, NULL);
76 
77 	for (f = 0; f < nconf_files; f++) {
78 		/* Collect the requests this file is responsible for */
79 		nopts = 0;
80 		for (n = 0; n < nreqs; n++) {
81 			size_t target;
82 
83 			if (reqs[n].remove) {
84 				if (!reqs[n].infile[f])
85 					continue;
86 			} else if (reqs[n].newvalue != NULL) {
87 				/*
88 				 * A directive whose only definition is
89 				 * the read-only defaults file is written
90 				 * like one defined nowhere: appended to
91 				 * the format's default write target.
92 				 */
93 				if (reqs[n].srcidx < 0 ||
94 				    reqs[n].srcidx == defaults_idx)
95 					target = write_idx;
96 				else
97 					target = (size_t)reqs[n].srcidx;
98 				if (target != f)
99 					continue;
100 				/*
101 				 * An identical make(1) `+=value' line is
102 				 * already present: nothing to write, but
103 				 * still report the effective value unless
104 				 * -q (see report loop for the SET case).
105 				 */
106 				if (reqs[n].op == BSDCONF_OP_APPEND &&
107 				    reqs[n].append_present) {
108 					if (!quiet)
109 						print_write_echo(conf_files[f],
110 						    0, reqs[n].name,
111 						    BSDCONF_OP_APPEND,
112 						    reqs[n].value,
113 						    reqs[n].value, 0, 0, 0);
114 					continue;
115 				}
116 				/*
117 				 * -s only ensures presence: if the
118 				 * WITH_/WITHOUT_ knob is already defined
119 				 * (any value), leave the file alone.
120 				 * An explicit name= (including empty)
121 				 * always writes the requested value.
122 				 */
123 				if (set_knobs && make_knob_p(reqs[n].name) &&
124 				    reqs[n].found) {
125 					if (!quiet)
126 						print_write_echo(conf_files[f],
127 						    0, reqs[n].name,
128 						    reqs[n].op,
129 						    reqs[n].value,
130 						    reqs[n].value, 0, 0,
131 						    0);
132 					continue;
133 				}
134 			} else
135 				continue;
136 			opt = &options[nopts];
137 			memset(opt, 0, sizeof(*opt));
138 			opt->type = BSDCONF_TYPE_STR;
139 			opt->directive = reqs[n].name;
140 			opt->op = reqs[n].op;
141 			if (reqs[n].remove) {
142 				opt->action = BSDCONF_ACTION_REMOVE;
143 				opt->value.str = NULL;
144 			} else {
145 				opt->action = BSDCONF_ACTION_SET_VALUE;
146 				opt->value.str =
147 				    (char *)(uintptr_t)reqs[n].newvalue;
148 			}
149 			owner[nopts++] = &reqs[n];
150 		}
151 		if (nopts == 0)
152 			continue;
153 		memset(&options[nopts], 0, sizeof(options[nopts]));
154 
155 		/*
156 		 * Create the target if it does not yet exist. O_EXCL makes
157 		 * the test-and-create atomic: an existing file (or one that
158 		 * appears concurrently) is left untouched.
159 		 */
160 		if ((fd = open(conf_files[f], O_WRONLY | O_CREAT | O_EXCL,
161 		    0666)) >= 0)
162 			close(fd);
163 		else if (errno != EEXIST)
164 			err(EXIT_FAILURE, "%s", conf_files[f]);
165 
166 		if (bsdconf_put(options, conf_files[f],
167 		    bsdconf_format_processing(format),
168 		    bsdconf_format_put(format)) != 0)
169 			err(EXIT_FAILURE, "%s", conf_files[f]);
170 
171 		/* Report the results */
172 		for (n = 0; n < nopts; n++) {
173 			opt = &options[n];
174 			if (opt->action == BSDCONF_ACTION_REMOVE) {
175 				if ((opt->result &
176 				    BSDCONF_DIRECTIVE_REMOVED) != 0) {
177 					owner[n]->found = 1;
178 					/*
179 					 * Removals are quiet unless -v/-V
180 					 * (or -q which is already silent).
181 					 */
182 					if (!quiet &&
183 					    (verbose || verbose_trail))
184 						print_write_echo(
185 						    conf_files[f],
186 						    opt->line,
187 						    opt->directive,
188 						    opt->op, "", "", 0, 0,
189 						    1);
190 				}
191 				continue;
192 			}
193 			if (!quiet) {
194 				const char *old = owner[n]->value;
195 				char *neweff = NULL;
196 				const char *shown;
197 
198 				/*
199 				 * Gate the echo on whether bsdconf_put()
200 				 * actually edited the file
201 				 * (BSDCONF_VALUE_CHANGED). With -V, shape the
202 				 * echo like a read trail
203 				 * (`file:line: name=old -> name=new'). For
204 				 * make(1) `+=', show the effective value
205 				 * before and after the append rather than
206 				 * the fragment alone.
207 				 */
208 				if (opt->op == BSDCONF_OP_APPEND) {
209 					neweff = make_apply(BSDCONF_OP_APPEND,
210 					    old, opt->value.str);
211 					if (neweff == NULL)
212 						err(EXIT_FAILURE, NULL);
213 					shown = neweff;
214 				} else
215 					shown = opt->value.str;
216 				/*
217 				 * -V shows statement shape at file:line; a
218 				 * newly added statement is `(added)' rather
219 				 * than `op -> op+value'. -v keeps effective
220 				 * old -> new for `+='.
221 				 */
222 				if (verbose_trail &&
223 				    (opt->result &
224 				    BSDCONF_DIRECTIVE_ADDED) != 0)
225 					print_write_echo(conf_files[f],
226 					    opt->line, opt->directive, opt->op,
227 					    "", opt->value.str,
228 					    (opt->result &
229 					    BSDCONF_VALUE_CHANGED) != 0, 1, 0);
230 				else if (verbose_trail)
231 					print_write_echo(conf_files[f],
232 					    opt->line, opt->directive, opt->op,
233 					    old == NULL ? "" : old, shown,
234 					    (opt->result &
235 					    BSDCONF_VALUE_CHANGED) != 0, 0, 0);
236 				else
237 					print_write_echo(conf_files[f],
238 					    opt->line, opt->directive, opt->op,
239 					    old, shown,
240 					    (opt->result &
241 					    BSDCONF_VALUE_CHANGED) != 0, 0, 0);
242 				if (neweff != NULL) {
243 					free(owner[n]->value);
244 					owner[n]->value = neweff;
245 				}
246 			}
247 		}
248 	}
249 
250 	/* A removal that matched no file at all is an unknown directive */
251 	for (n = 0; n < nreqs; n++) {
252 		if (!reqs[n].remove || reqs[n].found)
253 			continue;
254 		if (ignore_unknown)
255 			continue;
256 		if (!quiet)
257 			warnx("unknown directive '%s'", reqs[n].name);
258 		rv = EXIT_FAILURE;
259 	}
260 
261 	free(options);
262 	free(owner);
263 	return (rv);
264 }
265