xref: /freebsd/usr.sbin/sysconf/sysconf_edit_assign.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  * CLI assignment parsing and sysrc(8)-style list word edits.
10  */
11 
12 #include <ctype.h>
13 
14 #include "sysconf_priv.h"
15 
16 /*
17  * Split a `name[=value]' argument into a request. With `remove' set, the
18  * argument is a bare name to delete. For make(1)-style targets, a trailing
19  * operator character on the name (`+', `?', `:', `!') selects the
20  * corresponding assignment modifier: `+=' appends a new `name+=value' line
21  * (make accumulates; we never rewrite a prior assignment), while `=', `?=',
22  * `:=', and `!=' rewrite or add a single assignment as today. `name-=word'
23  * is a list strike: the last assignment whose value contains each word is
24  * rewritten (or removed if emptied); see do_make_strikes(). For every other
25  * target, `name+=word' and `name-=word' are sysrc(8)-style list edits
26  * against the effective value (see merge_list_requests() below).
27  */
28 void
split_request(char * arg,struct request * req,int remove)29 split_request(char *arg, struct request *req, int remove)
30 {
31 	char *eq;
32 	size_t n;
33 
34 	req->name = arg;
35 	req->newvalue = NULL;
36 	req->op = BSDCONF_OP_DEFAULT;
37 	req->listop = '\0';
38 	req->value = NULL;
39 	req->found = 0;
40 	req->append_present = 0;
41 	req->remove = remove;
42 	req->srcidx = -1;
43 	req->line = 0;
44 	req->trail = NULL;
45 	req->ntrail = 0;
46 	req->assigns = NULL;
47 	req->nassigns = 0;
48 	if ((req->infile = calloc(nconf_files, 1)) == NULL)
49 		err(EXIT_FAILURE, NULL);
50 
51 	if (remove) {
52 		if (strchr(arg, '=') != NULL)
53 			errx(EXIT_FAILURE,
54 			    "%s: -x does not take a value", arg);
55 		return;
56 	}
57 
58 	if ((eq = strchr(arg, '=')) == NULL)
59 		return;
60 
61 	*eq = '\0';
62 
63 	/*
64 	 * Strip one balanced layer of double-quotes from the value as a
65 	 * courtesy (e.g., a literal zfs_load="YES" from a quoted shell
66 	 * word); the library re-applies the target format's quoting rules
67 	 * on output.
68 	 */
69 	req->newvalue = bsdconf_unquote(eq + 1);
70 
71 	/* Split an operator character off the tail of the name */
72 	n = strlen(arg);
73 	if ((bsdconf_format_processing(format) &
74 	    BSDCONF_OPERATOR_EQUALS) != 0 && n > 1) {
75 		switch (arg[n - 1]) {
76 		case '+': req->op = BSDCONF_OP_APPEND; break;
77 		case '?': req->op = BSDCONF_OP_COND; break;
78 		case ':': req->op = BSDCONF_OP_EXPAND; break;
79 		case '!': req->op = BSDCONF_OP_SHELL; break;
80 		case '-':
81 			/* make(1) has no `-='; treat as a list strike */
82 			req->listop = '-';
83 			arg[n - 1] = '\0';
84 			break;
85 		}
86 		if (req->op != BSDCONF_OP_DEFAULT)
87 			arg[n - 1] = '\0';
88 	} else if (n > 1 && (arg[n - 1] == '+' || arg[n - 1] == '-')) {
89 		req->listop = arg[n - 1];
90 		arg[n - 1] = '\0';
91 	}
92 
93 	if (*arg == '\0')
94 		errx(EXIT_FAILURE, "empty directive name");
95 }
96 
97 /* True when `req' is a make/src `name-=word' strike (not a sysrc list edit). */
98 /*
99  * Test whether the whitespace-separated `list' contains `word' (of length
100  * `wlen') as a whole word.
101  */
102 int
list_has(const char * list,const char * word,size_t wlen)103 list_has(const char *list, const char *word, size_t wlen)
104 {
105 	size_t len;
106 
107 	if (list == NULL)
108 		return (0);
109 	while (*list != '\0') {
110 		list += strspn(list, " \t");
111 		if ((len = strcspn(list, " \t")) == 0)
112 			break;
113 		if (len == wlen && strncmp(list, word, wlen) == 0)
114 			return (1);
115 		list += len;
116 	}
117 
118 	return (0);
119 }
120 
121 /*
122  * Merge the whitespace-separated `words' into (op `+') or out of (op `-')
123  * the whitespace-separated list `current' (NULL for an unset directive),
124  * appending only words not already present and joining the survivors with
125  * single spaces. Returns newly allocated storage.
126  */
127 char *
list_merge(const char * current,const char * words,int op)128 list_merge(const char *current, const char *words, int op)
129 {
130 	size_t len;
131 	size_t rlen = 0;
132 	size_t size;
133 	char *result;
134 	const char *token;
135 
136 	if (current == NULL)
137 		current = "";
138 	size = strlen(current) + strlen(words) + 2;
139 	if ((result = malloc(size)) == NULL)
140 		err(EXIT_FAILURE, NULL);
141 	result[0] = '\0';
142 
143 	/* Copy the current words, striking matches under `-' */
144 	token = current;
145 	while (*token != '\0') {
146 		token += strspn(token, " \t");
147 		if ((len = strcspn(token, " \t")) == 0)
148 			break;
149 		if (op == '-' && list_has(words, token, len)) {
150 			token += len;
151 			continue;
152 		}
153 		if (rlen > 0)
154 			result[rlen++] = ' ';
155 		memcpy(result + rlen, token, len);
156 		rlen += len;
157 		result[rlen] = '\0';
158 		token += len;
159 	}
160 
161 	/* Append the new words not already present under `+' */
162 	if (op == '+') {
163 		token = words;
164 		while (*token != '\0') {
165 			token += strspn(token, " \t");
166 			if ((len = strcspn(token, " \t")) == 0)
167 				break;
168 			if (!list_has(result, token, len)) {
169 				if (rlen > 0)
170 					result[rlen++] = ' ';
171 				memcpy(result + rlen, token, len);
172 				rlen += len;
173 				result[rlen] = '\0';
174 			}
175 			token += len;
176 		}
177 	}
178 
179 	return (result);
180 }
181 
182 /*
183  * Materialize sysrc(8)-style list-edit requests (`name+=word ...' and
184  * `name-=word ...') against the effective values recorded by scan_pass(),
185  * rewriting each request's pending value in place so the ordinary write,
186  * check, and echo machinery sees a plain assignment. Make/src `name-=word'
187  * strikes are handled separately by do_make_strikes(). Appending to an
188  * unset directive creates it; striking words from one is reported like any
189  * other unknown directive (there is nothing to remove) and the request is
190  * neutralized. Returns EXIT_SUCCESS or EXIT_FAILURE.
191  */
192 int
merge_list_requests(void)193 merge_list_requests(void)
194 {
195 	int rv = EXIT_SUCCESS;
196 	unsigned int n;
197 
198 	for (n = 0; n < nreqs; n++) {
199 		if (reqs[n].listop == '\0' || reqs[n].newvalue == NULL)
200 			continue;
201 		/* make/src `-=` is a per-statement strike, not a list merge */
202 		if (make_strike_p(&reqs[n]))
203 			continue;
204 		if (reqs[n].listop == '-' && !reqs[n].found) {
205 			if (!ignore_unknown) {
206 				if (!quiet)
207 					warnx("unknown directive '%s'",
208 					    reqs[n].name);
209 				rv = EXIT_FAILURE;
210 			}
211 			/* Convert to a no-op */
212 			reqs[n].newvalue = NULL;
213 			reqs[n].name = "";
214 			continue;
215 		}
216 		reqs[n].newvalue = list_merge(reqs[n].value,
217 		    reqs[n].newvalue, reqs[n].listop);
218 	}
219 
220 	return (rv);
221 }
222