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