xref: /freebsd/lib/libbsdconf/bsdconf.c (revision 90ad63b540317ce7c349b5ebab065b40ba748c27)
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 <ctype.h>
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <fnmatch.h>
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <unistd.h>
16 
17 #include "bsdconf.h"
18 #include "bsdconf_internal.h"
19 
20 /*
21  * Search for a config option (struct bsdconf_option) in the array of config
22  * options, returning the struct whose directive matches the given parameter.
23  * If no match is found, NULL is returned.
24  *
25  * This is to eliminate dependency on the index position of an item in the
26  * array, since the index position is more apt to be changed as code grows.
27  */
28 struct bsdconf_option *
bsdconf_get_option(struct bsdconf_option options[],const char * directive)29 bsdconf_get_option(struct bsdconf_option options[], const char *directive)
30 {
31 	uint32_t n;
32 
33 	if (options == NULL || directive == NULL)
34 		return (NULL);
35 
36 	for (n = 0; options[n].directive != NULL; n++)
37 		if (strcmp(options[n].directive, directive) == 0)
38 			return (&options[n]);
39 
40 	return (NULL);
41 }
42 
43 /*
44  * Strip one layer of surrounding double-quotes from `value' in place (the
45  * parser preserves quotes so that values round-trip losslessly; see
46  * bsdconf_fparse() below). Returns `value' for convenience. If the value is
47  * not a quoted string, it is returned unmodified.
48  */
49 char *
bsdconf_unquote(char * value)50 bsdconf_unquote(char *value)
51 {
52 	size_t len;
53 
54 	if (value == NULL || (len = strlen(value)) < 2)
55 		return (value);
56 	if (value[0] != '"' || value[len - 1] != '"')
57 		return (value);
58 
59 	memmove(value, value + 1, len - 2);
60 	value[len - 2] = '\0';
61 
62 	return (value);
63 }
64 
65 /*
66  * Copy the remaining contents of the open file descriptor `fd' to an
67  * unlinked temporary file and return a seekable descriptor referencing it
68  * (which the caller must close(2); the backing storage is reclaimed then).
69  * This adapts input that cannot seek -- a pipe or socket, standard input
70  * included -- for the scanner in bsdconf_fparse() below, which seeks
71  * liberally. Returns the new descriptor on success; otherwise returns -1
72  * and errno should be consulted.
73  */
74 int
bsdconf_spool(int fd)75 bsdconf_spool(int fd)
76 {
77 	FILE *tmp;
78 	int error;
79 	int newfd;
80 	int tfd;
81 	ssize_t r;
82 	char buf[8192];
83 
84 	if ((tmp = tmpfile()) == NULL)
85 		return (-1);
86 	tfd = fileno(tmp);
87 
88 	for (;;) {
89 		r = read(fd, buf, sizeof(buf));
90 		if (r < 0) {
91 			if (errno == EINTR)
92 				continue;
93 			goto fail;
94 		}
95 		if (r == 0)
96 			break;
97 		if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
98 			goto fail;
99 	}
100 	if (lseek(tfd, 0, SEEK_SET) == -1)
101 		goto fail;
102 
103 	/* Detach the descriptor from the stream before closing it */
104 	if ((newfd = dup(tfd)) == -1)
105 		goto fail;
106 	fclose(tmp);
107 	return (newfd);
108 
109 fail:
110 	error = errno; /* preserve errno across fclose(3) */
111 	fclose(tmp);
112 	errno = error;
113 	return (-1);
114 }
115 
116 /*
117  * Read one byte into `*p', restarting on EINTR. Returns 1 on success, 0 on
118  * EOF, or -1 on error (with errno set). Callers must treat a negative return
119  * as failure: a loop conditioned only on `r != 0' spins forever on error
120  * because read(2) returns -1, and a length counter in such a loop can grow
121  * without bound (see the directive scan in bsdconf_fparse() below).
122  */
123 static ssize_t
bsdconf_read1(int fd,char * p)124 bsdconf_read1(int fd, char *p)
125 {
126 	ssize_t r;
127 
128 	do {
129 		r = read(fd, p, 1);
130 	} while (r < 0 && errno == EINTR);
131 	return (r);
132 }
133 
134 /*
135  * Read exactly `n' bytes into `buf', restarting on EINTR. Returns 0 on
136  * success, or -1 on error / premature EOF (with errno set; EIO for a short
137  * read after the caller measured a length on a seekable descriptor).
138  */
139 static int
bsdconf_readn(int fd,void * buf,size_t n)140 bsdconf_readn(int fd, void *buf, size_t n)
141 {
142 	char *p = buf;
143 	size_t off = 0;
144 	ssize_t r;
145 
146 	while (off < n) {
147 		r = read(fd, p + off, n - off);
148 		if (r < 0) {
149 			if (errno == EINTR)
150 				continue;
151 			return (-1);
152 		}
153 		if (r == 0) {
154 			errno = EIO;
155 			return (-1);
156 		}
157 		off += (size_t)r;
158 	}
159 	return (0);
160 }
161 
162 /*
163  * Advance past horizontal whitespace (spaces and tabs, not newline).
164  * Updates `*r' and the byte in `*p'. Returns 0 on success, or -1 on
165  * read error (errno set).
166  */
167 static int
bsdconf_skip_hspace(int fd,char * p,ssize_t * r)168 bsdconf_skip_hspace(int fd, char *p, ssize_t *r)
169 {
170 
171 	while (*r > 0 && isspace((unsigned char)*p) && *p != '\n') {
172 		*r = bsdconf_read1(fd, p);
173 		if (*r < 0)
174 			return (-1);
175 	}
176 	return (0);
177 }
178 
179 /*
180  * Truncate trailing whitespace from a NUL-terminated string whose end
181  * (the NUL) is at `end'. Returns a pointer to the last remaining
182  * character, or to `value' when the string is empty.
183  */
184 static char *
bsdconf_rtrim_ws(char * value,char * end)185 bsdconf_rtrim_ws(char *value, char *end)
186 {
187 	char *t = end;
188 
189 	while (t > value && isspace((unsigned char)*--t))
190 		*t = '\0';
191 	return (t);
192 }
193 
194 /*
195  * Drop a trailing inline `#' or unescaped `;' that rode along with the
196  * value (historic figpar behavior), then trim again. `ecomment' is set
197  * when the end-key scan stopped on an unquoted `#'.
198  */
199 static char *
bsdconf_trim_value_key(char * value,char * t,bool ecomment,bool bsemicolon)200 bsdconf_trim_value_key(char *value, char *t, bool ecomment, bool bsemicolon)
201 {
202 	uint32_t x;
203 
204 	if (ecomment && t > value && *t == '#') {
205 		*t = '\0';
206 		return (bsdconf_rtrim_ws(value, t));
207 	}
208 	if (bsemicolon && t > value && *t == ';') {
209 		for (x = 0; t - x > value && *(t - x - 1) == '\\'; x++)
210 			;
211 		if ((x & 1) == 0) {
212 			*t = '\0';
213 			return (bsdconf_rtrim_ws(value, t));
214 		}
215 	}
216 	return (t);
217 }
218 
219 /*
220  * Invoke the unknown-directive call-back with a stack-local option that
221  * carries the statement's assignment operator (there is no matched
222  * options[] slot to hang it on). Returns the call-back's result.
223  */
224 static int
bsdconf_call_unknown(int (* unknown)(struct bsdconf_option * option,uint32_t line,char * directive,char * value),enum bsdconf_op op,uint32_t dline,char * directive,char * value)225 bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
226     uint32_t line, char *directive, char *value), enum bsdconf_op op,
227     uint32_t dline, char *directive, char *value)
228 {
229 	struct bsdconf_option unk;
230 
231 	memset(&unk, 0, sizeof(unk));
232 	unk.op = op;
233 	return (unknown(&unk, dline, directive, value));
234 }
235 
236 /*
237  * Scan from the current byte in `*p' to the end of the value. Handles
238  * quotes, escaped end-keys, inline comments, and semicolon terminators.
239  * On return, `*p' holds the terminating key (or is at EOF), and `*r',
240  * `*line', `*comment', and `*ecomment' are updated. Returns 0 on
241  * success, or -1 on seek/read error (errno set).
242  */
243 static int
bsdconf_scan_value_end(int fd,char * p,ssize_t * r,uint32_t * line,uint8_t * comment,uint8_t * ecomment,bool bsemicolon)244 bsdconf_scan_value_end(int fd, char *p, ssize_t *r, uint32_t *line,
245     uint8_t *comment, uint8_t *ecomment, bool bsemicolon)
246 {
247 	uint8_t end = 0;
248 	uint8_t quote = 0;
249 	uint32_t n;
250 	off_t charpos;
251 
252 	*ecomment = 0;
253 	while (*r > 0 && end == 0) {
254 		/* Advance to the next character if we know we can */
255 		if (*p != '\"' && *p != '#' && *p != '\n' &&
256 		    (!bsemicolon || *p != ';')) {
257 			*r = bsdconf_read1(fd, p);
258 			if (*r < 0)
259 				return (-1);
260 			continue;
261 		}
262 
263 		/*
264 		 * If we get this far, we've hit an end-key
265 		 */
266 
267 		/* Get the current offset */
268 		if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
269 			return (-1);
270 		charpos--;
271 
272 		/*
273 		 * Go back so we can read the character before the key to
274 		 * check if the character is escaped (which means we should
275 		 * continue).
276 		 */
277 		if (lseek(fd, -2, SEEK_CUR) == -1)
278 			return (-1);
279 		*r = bsdconf_read1(fd, p);
280 		if (*r < 0)
281 			return (-1);
282 
283 		/*
284 		 * Count how many backslashes there are (an odd number means
285 		 * the key is escaped, even means otherwise).
286 		 */
287 		for (n = 1; *r > 0 && *p == '\\'; n++) {
288 			/* Move back another offset to read */
289 			if (lseek(fd, -2, SEEK_CUR) == -1)
290 				return (-1);
291 			*r = bsdconf_read1(fd, p);
292 			if (*r < 0)
293 				return (-1);
294 		}
295 
296 		/* Move offset back to the key and read it */
297 		if (lseek(fd, charpos, SEEK_SET) == -1)
298 			return (-1);
299 		*r = bsdconf_read1(fd, p);
300 		if (*r < 0)
301 			return (-1);
302 
303 		/*
304 		 * If an even number of backslashes was counted meaning key
305 		 * is not escaped, we should evaluate what to do.
306 		 */
307 		if ((n & 1) == 1) {
308 			switch (*p) {
309 			case '\"':
310 				/*
311 				 * Flag current sequence of characters to
312 				 * follow as being quoted (hashes are not
313 				 * considered comments).
314 				 */
315 				quote = !quote;
316 				break;
317 			case '#':
318 				/*
319 				 * If we aren't in a quoted series, we just
320 				 * hit an inline comment and have found the
321 				 * end of the value. Flag the remainder of
322 				 * the line as a comment so it is not
323 				 * mistaken for a new directive.
324 				 */
325 				if (!quote) {
326 					*ecomment = *comment = 1;
327 					end = 1;
328 				}
329 				break;
330 			case '\n':
331 				/*
332 				 * Newline characters must always be escaped,
333 				 * whether inside a quoted series or not,
334 				 * otherwise they terminate the value.
335 				 */
336 				(*line)++;
337 				end = 1;
338 				/* FALLTHROUGH */
339 			case ';':
340 				if (!quote && bsemicolon)
341 					end = 1;
342 				break;
343 			}
344 		} else if (*p == '\n')
345 			/* Escaped newline character. increment */
346 			(*line)++;
347 
348 		/* Advance to the next character */
349 		*r = bsdconf_read1(fd, p);
350 		if (*r < 0)
351 			return (-1);
352 	}
353 	return (0);
354 }
355 
356 /*
357  * Parse the configuration data on the open file descriptor `fd' and execute
358  * the `parse' call-back functions for any directives defined by the array of
359  * config options (first argument).
360  *
361  * For unknown directives that are encountered, you can optionally pass a
362  * call-back function for the third argument to be called for unknowns.
363  *
364  * The scanner requires a seekable descriptor; input that cannot seek (a
365  * pipe or socket, standard input included) is detected up front and spooled
366  * through bsdconf_spool() above, parsed from the temporary, and costs one
367  * transient copy of the data. The descriptor is left positioned at
368  * end-of-file (non-seekable input is left drained) and remains open (the
369  * caller retains ownership).
370  *
371  * Returns zero on success; otherwise returns -1 (or the non-zero result of a
372  * call-back) and errno should be consulted.
373  */
374 int
bsdconf_fparse(struct bsdconf_option options[],int fd,int (* unknown)(struct bsdconf_option * option,uint32_t line,char * directive,char * value),uint16_t processing_options)375 bsdconf_fparse(struct bsdconf_option options[], int fd,
376     int (*unknown)(struct bsdconf_option *option, uint32_t line,
377     char *directive, char *value), uint16_t processing_options)
378 {
379 	bool bequals;
380 	bool bsemicolon;
381 	bool case_sensitive;
382 	bool operator_equals;
383 	bool require_equals;
384 	bool strict_equals;
385 	uint8_t comment = 0;
386 	uint8_t ecomment;
387 	uint8_t found;
388 	uint8_t have_equals = 0;
389 	char p[2];
390 	char *directive = NULL;
391 	char *t;
392 	char *value = NULL;
393 	enum bsdconf_op op;
394 	int error;
395 	int rv = 0;
396 	int spoolfd = -1;
397 	ssize_t r = 1;
398 	uint32_t dline;
399 	uint32_t dsize = 0;
400 	uint32_t line = 1;
401 	uint32_t n;
402 	uint32_t vsize = 0;
403 	uint32_t x;
404 	off_t charpos;
405 	off_t curpos;
406 
407 	/* Sanity check: if no options and no unknown function, return */
408 	if (options == NULL && unknown == NULL) {
409 		errno = EINVAL;
410 		return (-1);
411 	}
412 
413 	/* Spool input that cannot seek (see bsdconf_spool() above) */
414 	if (lseek(fd, 0, SEEK_CUR) == -1) {
415 		if (errno != ESPIPE)
416 			return (-1);
417 		if ((spoolfd = bsdconf_spool(fd)) == -1)
418 			return (-1);
419 		fd = spoolfd;
420 	}
421 
422 	/* Processing options */
423 	bequals = processing_options & BSDCONF_BREAK_ON_EQUALS;
424 	bsemicolon = processing_options & BSDCONF_BREAK_ON_SEMICOLON;
425 	case_sensitive = processing_options & BSDCONF_CASE_SENSITIVE;
426 	operator_equals = processing_options & BSDCONF_OPERATOR_EQUALS;
427 	require_equals = processing_options & BSDCONF_REQUIRE_EQUALS;
428 	strict_equals = processing_options & BSDCONF_STRICT_EQUALS;
429 
430 	/* Read the file until EOF */
431 	while (r > 0) {
432 		r = bsdconf_read1(fd, p);
433 		if (r < 0)
434 			goto fail;
435 
436 		/* Skip to the beginning of a directive */
437 		while (r > 0 && (isspace((unsigned char)*p) || *p == '#' ||
438 		    comment || (bsemicolon && *p == ';'))) {
439 			if (*p == '#')
440 				comment = 1;
441 			else if (*p == '\n') {
442 				comment = 0;
443 				line++;
444 			}
445 			r = bsdconf_read1(fd, p);
446 			if (r < 0)
447 				goto fail;
448 		}
449 		/* Test for EOF; if EOF then no directive was found */
450 		if (r == 0)
451 			goto cleanup;
452 
453 		/* Record the line number the directive appears on */
454 		dline = line;
455 
456 		/* Get the current offset */
457 		if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
458 			goto fail;
459 		curpos--;
460 
461 		/* Find the length of the directive */
462 		for (n = 0; r > 0; n++) {
463 			if (isspace((unsigned char)*p))
464 				break;
465 			if (bequals && *p == '=') {
466 				have_equals = 1;
467 				break;
468 			}
469 			if (bsemicolon && *p == ';')
470 				break;
471 			r = bsdconf_read1(fd, p);
472 			if (r < 0)
473 				goto fail;
474 		}
475 
476 		/* Test for EOF, if EOF then no directive was found */
477 		if (n == 0 && r == 0)
478 			goto cleanup;
479 
480 		/* Go back to the beginning of the directive */
481 		if (lseek(fd, curpos, SEEK_SET) == -1)
482 			goto fail;
483 
484 		/*
485 		 * Allocate and read the directive into memory. The buffer
486 		 * must be grown on the first pass (directive == NULL) even
487 		 * when the name is empty (a line beginning with `='), lest
488 		 * the string terminator below store through a NULL pointer.
489 		 */
490 		if (directive == NULL || n > dsize) {
491 			if ((t = realloc(directive, n + 1)) == NULL)
492 				goto fail;
493 			directive = t;
494 			dsize = n;
495 		}
496 		if (bsdconf_readn(fd, directive, n) != 0)
497 			goto fail;
498 
499 		/* Advance beyond the equals sign if appropriate/desired */
500 		if (bequals && *p == '=') {
501 			if (lseek(fd, 1, SEEK_CUR) != -1) {
502 				r = bsdconf_read1(fd, p);
503 				if (r < 0)
504 					goto fail;
505 			}
506 			if (strict_equals && isspace((unsigned char)*p))
507 				*p = '\n';
508 		}
509 
510 		/* Terminate the string */
511 		directive[n] = '\0';
512 
513 		/*
514 		 * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off
515 		 * the tail of the directive if requested. The operator
516 		 * character rode along with the directive because only the
517 		 * `=' terminates the directive scan (above).
518 		 */
519 		op = have_equals ? BSDCONF_OP_ASSIGN : BSDCONF_OP_DEFAULT;
520 		if (operator_equals && have_equals && n > 1) {
521 			switch (directive[n - 1]) {
522 			case '+': op = BSDCONF_OP_APPEND; break;
523 			case '?': op = BSDCONF_OP_COND; break;
524 			case ':': op = BSDCONF_OP_EXPAND; break;
525 			case '!': op = BSDCONF_OP_SHELL; break;
526 			}
527 			if (op != BSDCONF_OP_ASSIGN)
528 				directive[--n] = '\0';
529 		}
530 
531 		/* Convert directive to lower case before comparison */
532 		if (!case_sensitive)
533 			bsdconf_strtolower(directive);
534 
535 		/* Move to what may be the start of the value */
536 		if (!(bsemicolon && *p == ';') &&
537 		    !(strict_equals && *p == '=')) {
538 			if (bsdconf_skip_hspace(fd, p, &r) != 0)
539 				goto fail;
540 		}
541 
542 		/* An equals sign may have stopped us, should we eat it? */
543 		if (r > 0 && bequals && *p == '=' && !strict_equals) {
544 			have_equals = 1;
545 			r = bsdconf_read1(fd, p);
546 			if (r < 0)
547 				goto fail;
548 			if (bsdconf_skip_hspace(fd, p, &r) != 0)
549 				goto fail;
550 		}
551 
552 		/* If no value, allocate a dummy value and jump to action */
553 		if (r == 0 || *p == '\n' || *p == '#' ||
554 		    (bsemicolon && *p == ';')) {
555 			/* Count the consumed terminator if a newline */
556 			if (r > 0 && *p == '\n')
557 				line++;
558 			/* Flag a trailing comment so it is skipped */
559 			if (r > 0 && *p == '#')
560 				comment = 1;
561 			/* Initialize the value if not already done */
562 			if (value == NULL && (value = malloc(1)) == NULL)
563 				goto fail;
564 			value[0] = '\0';
565 			goto call_function;
566 		}
567 
568 		/* Get the current offset */
569 		if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
570 			goto fail;
571 		curpos--;
572 
573 		/* Find the end of the value */
574 		if (bsdconf_scan_value_end(fd, p, &r, &line, &comment,
575 		    &ecomment, bsemicolon) != 0)
576 			goto fail;
577 
578 		/* Get the current offset */
579 		if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
580 			goto fail;
581 
582 		/* Get the length of the value */
583 		n = (uint32_t)(charpos - curpos);
584 		if (r > 0) /* more to read, but don't read ending key */
585 			n--;
586 
587 		/* Move offset back to the beginning of the value */
588 		if (lseek(fd, curpos, SEEK_SET) == -1)
589 			goto fail;
590 
591 		/* Allocate and read the value into memory */
592 		if (n > vsize) {
593 			if ((t = realloc(value, n + 1)) == NULL)
594 				goto fail;
595 			value = t;
596 			vsize = n;
597 		}
598 		if (bsdconf_readn(fd, value, n) != 0)
599 			goto fail;
600 
601 		/* Terminate the string */
602 		value[n] = '\0';
603 
604 		/* Cut trailing whitespace and a trailing `#' / `;' key */
605 		t = bsdconf_rtrim_ws(value, value + n);
606 		t = bsdconf_trim_value_key(value, t, ecomment != 0,
607 		    bsemicolon);
608 
609 		/* Escape the escaped quotes (see bsdconf_string.c) */
610 		x = bsdconf_strcount(value, "\\\"");
611 		if (x != 0 && (n + x) > vsize) {
612 			if ((t = realloc(value, n + x + 1)) == NULL)
613 				goto fail;
614 			value = t;
615 			vsize = n + x;
616 		}
617 		if (bsdconf_replaceall(value, vsize + 1, "\\\"", "\\\\\"") < 0)
618 			goto fail;
619 
620 		/* Remove all escaped newline characters */
621 		if (bsdconf_replaceall(value, vsize + 1, "\\\n", "") < 0)
622 			goto fail;
623 
624 		/* Resolve escape sequences */
625 		bsdconf_strunexpand(value, value);
626 
627 call_function:
628 		/* Abort if we're seeking only assignments */
629 		if (require_equals && !have_equals) {
630 			errno = EINVAL;
631 			goto fail;
632 		}
633 
634 		found = have_equals = 0; /* reset */
635 
636 		/*
637 		 * Report the statement's assignment operator through a
638 		 * stack-local option when invoking the unknown call-back
639 		 * (there is no matched options[] slot to hang it on).
640 		 */
641 		if (options == NULL && unknown != NULL) {
642 			error = bsdconf_call_unknown(unknown, op, dline,
643 			    directive, value);
644 			if (error != 0) {
645 				rv = error;
646 				goto cleanup;
647 			}
648 			continue;
649 		}
650 
651 		/* Loop through the array looking for a match */
652 		for (n = 0; options[n].directive != NULL; n++) {
653 			error = fnmatch(options[n].directive, directive,
654 			    FNM_NOESCAPE);
655 			if (error == 0) {
656 				found = 1;
657 				/* Call function for array index item */
658 				options[n].op = op;
659 				if (options[n].parse != NULL) {
660 					error = options[n].parse(&options[n],
661 					    dline, directive, value);
662 					if (error != 0) {
663 						rv = error;
664 						goto cleanup;
665 					}
666 				}
667 			} else if (error != FNM_NOMATCH) {
668 				/* An error has occurred */
669 				errno = EINVAL;
670 				goto fail;
671 			}
672 		}
673 		if (!found && unknown != NULL) {
674 			/*
675 			 * No match was found for the value we read from the
676 			 * file; call function designated for unknown values.
677 			 */
678 			error = bsdconf_call_unknown(unknown, op, dline,
679 			    directive, value);
680 			if (error != 0) {
681 				rv = error;
682 				goto cleanup;
683 			}
684 		}
685 	}
686 
687 	goto cleanup;
688 
689 fail:
690 	rv = -1;
691 
692 cleanup:
693 	x = errno; /* preserve errno across free(3) and close(2) */
694 	if (spoolfd != -1)
695 		close(spoolfd);
696 	free(directive);
697 	free(value);
698 	errno = x;
699 
700 	return (rv);
701 }
702 
703 /*
704  * Parse the configuration file at `path' and execute the `parse' call-back
705  * functions for any directives defined by the array of config options (first
706  * argument). This is a convenience wrapper around bsdconf_fparse() above.
707  *
708  * Returns zero on success; otherwise returns -1 (or the non-zero result of a
709  * call-back) and errno should be consulted.
710  */
711 int
bsdconf_parse(struct bsdconf_option options[],const char * path,int (* unknown)(struct bsdconf_option * option,uint32_t line,char * directive,char * value),uint16_t processing_options)712 bsdconf_parse(struct bsdconf_option options[], const char *path,
713     int (*unknown)(struct bsdconf_option *option, uint32_t line,
714     char *directive, char *value), uint16_t processing_options)
715 {
716 	int error;
717 	int fd;
718 
719 	/* Sanity check: if no options and no unknown function, return */
720 	if (path == NULL || (options == NULL && unknown == NULL)) {
721 		errno = EINVAL;
722 		return (-1);
723 	}
724 
725 	/* Open the file */
726 	if ((fd = open(path, O_RDONLY)) < 0)
727 		return (-1);
728 
729 	error = bsdconf_fparse(options, fd, unknown, processing_options);
730 
731 	close(fd);
732 	return (error);
733 }
734