xref: /freebsd/lib/libbsdconf/bsdconf.c (revision 74cab6e2371fc35b58d5d3d018c7c3ad52809153)
1 /*
2  * Copyright (c) 2002-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 #include <errno.h>
9 #include <fcntl.h>
10 #include <fnmatch.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <unistd.h>
15 
16 #include "bsdconf.h"
17 #include "bsdconf_internal.h"
18 
19 /*
20  * Search for a config option (struct bsdconf_option) in the array of config
21  * options, returning the struct whose directive matches the given parameter.
22  * If no match is found, NULL is returned.
23  *
24  * This is to eliminate dependency on the index position of an item in the
25  * array, since the index position is more apt to be changed as code grows.
26  */
27 struct bsdconf_option *
28 bsdconf_get_option(struct bsdconf_option options[], const char *directive)
29 {
30 	uint32_t n;
31 
32 	if (options == NULL || directive == NULL)
33 		return (NULL);
34 
35 	for (n = 0; options[n].directive != NULL; n++)
36 		if (strcmp(options[n].directive, directive) == 0)
37 			return (&options[n]);
38 
39 	return (NULL);
40 }
41 
42 /*
43  * Strip one layer of surrounding double-quotes from `value' in place (the
44  * parser preserves quotes so that values round-trip losslessly; see
45  * bsdconf_fparse() below). Returns `value' for convenience. If the value is
46  * not a quoted string, it is returned unmodified.
47  */
48 char *
49 bsdconf_unquote(char *value)
50 {
51 	size_t len;
52 
53 	if (value == NULL || (len = strlen(value)) < 2)
54 		return (value);
55 	if (value[0] != '"' || value[len - 1] != '"')
56 		return (value);
57 
58 	memmove(value, value + 1, len - 2);
59 	value[len - 2] = '\0';
60 
61 	return (value);
62 }
63 
64 /*
65  * Copy the remaining contents of the open file descriptor `fd' to an
66  * unlinked temporary file and return a seekable descriptor referencing it
67  * (which the caller must close(2); the backing storage is reclaimed then).
68  * Exported for callers that need a seekable snapshot of a pipe or socket.
69  * Returns the new descriptor on success; otherwise returns -1 and errno
70  * should be consulted.
71  */
72 int
73 bsdconf_spool(int fd)
74 {
75 	FILE *tmp;
76 	int error;
77 	int newfd;
78 	int tfd;
79 	ssize_t r;
80 	char buf[8192];
81 
82 	if ((tmp = tmpfile()) == NULL)
83 		return (-1);
84 	tfd = fileno(tmp);
85 
86 	for (;;) {
87 		r = read(fd, buf, sizeof(buf));
88 		if (r < 0) {
89 			if (errno == EINTR)
90 				continue;
91 			goto fail;
92 		}
93 		if (r == 0)
94 			break;
95 		if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
96 			goto fail;
97 	}
98 	if (lseek(tfd, 0, SEEK_SET) == -1)
99 		goto fail;
100 
101 	/* Detach the descriptor from the stream before closing it */
102 	if ((newfd = dup(tfd)) == -1)
103 		goto fail;
104 	fclose(tmp);
105 	return (newfd);
106 
107 fail:
108 	error = errno; /* preserve errno across fclose(3) */
109 	fclose(tmp);
110 	errno = error;
111 	return (-1);
112 }
113 
114 /*
115  * Invoke the unknown-directive call-back with a stack-local option that
116  * carries the statement's assignment operator (there is no matched
117  * options[] slot to hang it on). Returns the call-back's result.
118  */
119 static int
120 bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
121     uint32_t line, char *directive, char *value), enum bsdconf_op op,
122     uint32_t dline, char *directive, char *value)
123 {
124 	struct bsdconf_option unk;
125 
126 	memset(&unk, 0, sizeof(unk));
127 	unk.op = op;
128 	return (unknown(&unk, dline, directive, value));
129 }
130 
131 /*
132  * Parse the configuration data on the open file descriptor `fd' and execute
133  * the `parse' call-back functions for any directives defined by the array of
134  * config options (first argument).
135  *
136  * For unknown directives that are encountered, you can optionally pass a
137  * call-back function for the third argument to be called for unknowns.
138  *
139  * The descriptor is read into a bounded in-memory buffer (see
140  * bsdconf_slurp()) and then scanned as an array of characters with
141  * bsdconf_scan(), the same tokenizer used by bsdconf_put(). The descriptor
142  * need not be seekable; a pipe or socket is read to EOF subject to the
143  * BSDCONF_MAX_BYTES cap. The descriptor is left positioned at end-of-file
144  * (non-seekable input is left drained) and remains open (the caller retains
145  * ownership).
146  *
147  * Returns zero on success; otherwise returns -1 (or the non-zero result of a
148  * call-back) and errno should be consulted.
149  */
150 int
151 bsdconf_fparse(struct bsdconf_option options[], int fd,
152     int (*unknown)(struct bsdconf_option *option, uint32_t line,
153     char *directive, char *value), uint16_t processing_options)
154 {
155 	bool bequals;
156 	bool bsemicolon;
157 	bool case_sensitive;
158 	bool found;
159 	bool operator_equals;
160 	bool require_equals;
161 	bool strict_equals;
162 	char *buf = NULL;
163 	char *directive = NULL;
164 	char *t;
165 	char *value = NULL;
166 	enum bsdconf_op op;
167 	int error;
168 	int rv = 0;
169 	int sverrno;
170 	size_t buflen = 0;
171 	size_t dsize = 0;
172 	size_t i = 0;
173 	size_t n;
174 	size_t vsize = 0;
175 	struct bsdconf_stmt st;
176 	uint32_t line = 1;
177 	unsigned int x;
178 
179 	/* Sanity check: if no options and no unknown function, return */
180 	if (options == NULL && unknown == NULL) {
181 		errno = EINVAL;
182 		return (-1);
183 	}
184 
185 	if ((buf = bsdconf_slurp(fd, &buflen)) == NULL)
186 		return (-1);
187 
188 	/* Processing options */
189 	bequals = processing_options & BSDCONF_BREAK_ON_EQUALS;
190 	bsemicolon = processing_options & BSDCONF_BREAK_ON_SEMICOLON;
191 	case_sensitive = processing_options & BSDCONF_CASE_SENSITIVE;
192 	operator_equals = processing_options & BSDCONF_OPERATOR_EQUALS;
193 	require_equals = processing_options & BSDCONF_REQUIRE_EQUALS;
194 	strict_equals = processing_options & BSDCONF_STRICT_EQUALS;
195 
196 	while (bsdconf_scan(buf, buflen, &i, &line, bequals, bsemicolon,
197 	    strict_equals, operator_equals, &st)) {
198 		n = st.dir_end - st.dir_start;
199 		if (directive == NULL || n > dsize) {
200 			if ((t = realloc(directive, n + 1)) == NULL)
201 				goto fail;
202 			directive = t;
203 			dsize = n;
204 		}
205 		memcpy(directive, buf + st.dir_start, n);
206 		directive[n] = '\0';
207 
208 		op = st.op;
209 		if (!case_sensitive)
210 			bsdconf_strtolower(directive);
211 
212 		if (!st.have_value) {
213 			if (value == NULL && (value = malloc(1)) == NULL)
214 				goto fail;
215 			value[0] = '\0';
216 			goto call_function;
217 		}
218 
219 		n = st.val_end - st.val_start;
220 		if (value == NULL || n > vsize) {
221 			if ((t = realloc(value, n + 1)) == NULL)
222 				goto fail;
223 			value = t;
224 			vsize = n;
225 		}
226 		memcpy(value, buf + st.val_start, n);
227 		value[n] = '\0';
228 
229 		/* Escape the escaped quotes (see bsdconf_string.c) */
230 		x = bsdconf_strcount(value, "\\\"");
231 		if (x != 0 && (n + x) > vsize) {
232 			if ((t = realloc(value, n + x + 1)) == NULL)
233 				goto fail;
234 			value = t;
235 			vsize = n + x;
236 		}
237 		if (bsdconf_replaceall(value, vsize + 1, "\\\"", "\\\\\"") < 0)
238 			goto fail;
239 
240 		/* Remove all escaped newline characters */
241 		if (bsdconf_replaceall(value, vsize + 1, "\\\n", "") < 0)
242 			goto fail;
243 
244 		/* Resolve escape sequences */
245 		bsdconf_strunexpand(value, value);
246 
247 call_function:
248 		/* Abort if we're seeking only assignments */
249 		if (require_equals && !st.have_equals) {
250 			errno = EINVAL;
251 			goto fail;
252 		}
253 
254 		found = 0;
255 
256 		/*
257 		 * Report the statement's assignment operator through a
258 		 * stack-local option when invoking the unknown call-back
259 		 * (there is no matched options[] slot to hang it on).
260 		 */
261 		if (options == NULL && unknown != NULL) {
262 			error = bsdconf_call_unknown(unknown, op, st.line,
263 			    directive, value);
264 			if (error != 0) {
265 				rv = error;
266 				goto cleanup;
267 			}
268 			continue;
269 		}
270 
271 		/* Loop through the array looking for a match */
272 		for (n = 0; options[n].directive != NULL; n++) {
273 			error = fnmatch(options[n].directive, directive,
274 			    FNM_NOESCAPE);
275 			if (error == 0) {
276 				found = 1;
277 				/* Call function for array index item */
278 				options[n].op = op;
279 				if (options[n].parse != NULL) {
280 					error = options[n].parse(&options[n],
281 					    st.line, directive, value);
282 					if (error != 0) {
283 						rv = error;
284 						goto cleanup;
285 					}
286 				}
287 			} else if (error != FNM_NOMATCH) {
288 				/* An error has occurred */
289 				errno = EINVAL;
290 				goto fail;
291 			}
292 		}
293 		if (!found && unknown != NULL) {
294 			/*
295 			 * No match was found for the value we read from the
296 			 * file; call function designated for unknown values.
297 			 */
298 			error = bsdconf_call_unknown(unknown, op, st.line,
299 			    directive, value);
300 			if (error != 0) {
301 				rv = error;
302 				goto cleanup;
303 			}
304 		}
305 	}
306 
307 	goto cleanup;
308 
309 fail:
310 	rv = -1;
311 
312 cleanup:
313 	sverrno = errno; /* preserve errno across free(3) */
314 	free(buf);
315 	free(directive);
316 	free(value);
317 	errno = sverrno;
318 
319 	return (rv);
320 }
321 
322 /*
323  * Parse the configuration file at `path' and execute the `parse' call-back
324  * functions for any directives defined by the array of config options (first
325  * argument). This is a convenience wrapper around bsdconf_fparse() above.
326  *
327  * Returns zero on success; otherwise returns -1 (or the non-zero result of a
328  * call-back) and errno should be consulted.
329  */
330 int
331 bsdconf_parse(struct bsdconf_option options[], const char *path,
332     int (*unknown)(struct bsdconf_option *option, uint32_t line,
333     char *directive, char *value), uint16_t processing_options)
334 {
335 	int error;
336 	int fd;
337 
338 	/* Sanity check: if no options and no unknown function, return */
339 	if (path == NULL || (options == NULL && unknown == NULL)) {
340 		errno = EINVAL;
341 		return (-1);
342 	}
343 
344 	/* Open the file */
345 	if ((fd = open(path, O_RDONLY)) < 0)
346 		return (-1);
347 
348 	error = bsdconf_fparse(options, fd, unknown, processing_options);
349 
350 	close(fd);
351 	return (error);
352 }
353