1/*-
2 * Copyright (c) 2002-2014 Devin Teske <dteske@FreeBSD.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 * SUCH DAMAGE.
25 */
26
27#include <sys/cdefs.h>
28__FBSDID("$FreeBSD: releng/10.2/lib/libfigpar/figpar.c 274116 2014-11-04 23:46:01Z dteske $");
29
30#include <sys/param.h>
31
32#include <ctype.h>
33#include <errno.h>
34#include <fcntl.h>
35#include <fnmatch.h>
36#include <stdlib.h>
37#include <string.h>
38#include <unistd.h>
39
40#include "figpar.h"
41#include "string_m.h"
42
43struct fp_config fp_dummy_config = {0, NULL, {0}, NULL};
44
45/*
46 * Search for config option (struct fp_config) in the array of config options,
47 * returning the struct whose directive matches the given parameter. If no
48 * match is found, a pointer to the static dummy array (above) is returned.
49 *
50 * This is to eliminate dependency on the index position of an item in the
51 * array, since the index position is more apt to be changed as code grows.
52 */
53struct fp_config *
54get_config_option(struct fp_config options[], const char *directive)
55{
56	uint32_t n;
57
58	/* Check arguments */
59	if (options == NULL || directive == NULL)
60		return (&fp_dummy_config);
61
62	/* Loop through the array, return the index of the first match */
63	for (n = 0; options[n].directive != NULL; n++)
64		if (strcmp(options[n].directive, directive) == 0)
65			return (&(options[n]));
66
67	/* Re-initialize the dummy variable in case it was written to */
68	fp_dummy_config.directive	= NULL;
69	fp_dummy_config.type		= 0;
70	fp_dummy_config.action		= NULL;
71	fp_dummy_config.value.u_num	= 0;
72
73	return (&fp_dummy_config);
74}
75
76/*
77 * Parse the configuration file at `path' and execute the `action' call-back
78 * functions for any directives defined by the array of config options (first
79 * argument).
80 *
81 * For unknown directives that are encountered, you can optionally pass a
82 * call-back function for the third argument to be called for unknowns.
83 *
84 * Returns zero on success; otherwise returns -1 and errno should be consulted.
85*/
86int
87parse_config(struct fp_config options[], const char *path,
88    int (*unknown)(struct fp_config *option, uint32_t line, char *directive,
89    char *value), uint16_t processing_options)
90{
91	uint8_t bequals;
92	uint8_t bsemicolon;
93	uint8_t case_sensitive;
94	uint8_t comment = 0;
95	uint8_t end;
96	uint8_t found;
97	uint8_t have_equals = 0;
98	uint8_t quote;
99	uint8_t require_equals;
100	uint8_t strict_equals;
101	char p[2];
102	char *directive;
103	char *t;
104	char *value;
105	int error;
106	int fd;
107	ssize_t r = 1;
108	uint32_t dsize;
109	uint32_t line = 1;
110	uint32_t n;
111	uint32_t vsize;
112	uint32_t x;
113	off_t charpos;
114	off_t curpos;
115	char rpath[PATH_MAX];
116
117	/* Sanity check: if no options and no unknown function, return */
118	if (options == NULL && unknown == NULL)
119		return (-1);
120
121	/* Processing options */
122	bequals = (processing_options & FP_BREAK_ON_EQUALS) == 0 ? 0 : 1;
123	bsemicolon = (processing_options & FP_BREAK_ON_SEMICOLON) == 0 ? 0 : 1;
124	case_sensitive = (processing_options & FP_CASE_SENSITIVE) == 0 ? 0 : 1;
125	require_equals = (processing_options & FP_REQUIRE_EQUALS) == 0 ? 0 : 1;
126	strict_equals = (processing_options & FP_STRICT_EQUALS) == 0 ? 0 : 1;
127
128	/* Initialize strings */
129	directive = value = 0;
130	vsize = dsize = 0;
131
132	/* Resolve the file path */
133	if (realpath(path, rpath) == 0)
134		return (-1);
135
136	/* Open the file */
137	if ((fd = open(rpath, O_RDONLY)) < 0)
138		return (-1);
139
140	/* Read the file until EOF */
141	while (r != 0) {
142		r = read(fd, p, 1);
143
144		/* skip to the beginning of a directive */
145		while (r != 0 && (isspace(*p) || *p == '#' || comment ||
146		    (bsemicolon && *p == ';'))) {
147			if (*p == '#')
148				comment = 1;
149			else if (*p == '\n') {
150				comment = 0;
151				line++;
152			}
153			r = read(fd, p, 1);
154		}
155		/* Test for EOF; if EOF then no directive was found */
156		if (r == 0) {
157			close(fd);
158			return (0);
159		}
160
161		/* Get the current offset */
162		curpos = lseek(fd, 0, SEEK_CUR) - 1;
163		if (curpos == -1) {
164			close(fd);
165			return (-1);
166		}
167
168		/* Find the length of the directive */
169		for (n = 0; r != 0; n++) {
170			if (isspace(*p))
171				break;
172			if (bequals && *p == '=') {
173				have_equals = 1;
174				break;
175			}
176			if (bsemicolon && *p == ';')
177				break;
178			r = read(fd, p, 1);
179		}
180
181		/* Test for EOF, if EOF then no directive was found */
182		if (n == 0 && r == 0) {
183			close(fd);
184			return (0);
185		}
186
187		/* Go back to the beginning of the directive */
188		error = (int)lseek(fd, curpos, SEEK_SET);
189		if (error == (curpos - 1)) {
190			close(fd);
191			return (-1);
192		}
193
194		/* Allocate and read the directive into memory */
195		if (n > dsize) {
196			if ((directive = realloc(directive, n + 1)) == NULL) {
197				close(fd);
198				return (-1);
199			}
200			dsize = n;
201		}
202		r = read(fd, directive, n);
203
204		/* Advance beyond the equals sign if appropriate/desired */
205		if (bequals && *p == '=') {
206			if (lseek(fd, 1, SEEK_CUR) != -1)
207				r = read(fd, p, 1);
208			if (strict_equals && isspace(*p))
209				*p = '\n';
210		}
211
212		/* Terminate the string */
213		directive[n] = '\0';
214
215		/* Convert directive to lower case before comparison */
216		if (!case_sensitive)
217			strtolower(directive);
218
219		/* Move to what may be the start of the value */
220		if (!(bsemicolon && *p == ';') &&
221		    !(strict_equals && *p == '=')) {
222			while (r != 0 && isspace(*p) && *p != '\n')
223				r = read(fd, p, 1);
224		}
225
226		/* An equals sign may have stopped us, should we eat it? */
227		if (r != 0 && bequals && *p == '=' && !strict_equals) {
228			have_equals = 1;
229			r = read(fd, p, 1);
230			while (r != 0 && isspace(*p) && *p != '\n')
231				r = read(fd, p, 1);
232		}
233
234		/* If no value, allocate a dummy value and jump to action */
235		if (r == 0 || *p == '\n' || *p == '#' ||
236		    (bsemicolon && *p == ';')) {
237			/* Initialize the value if not already done */
238			if (value == NULL && (value = malloc(1)) == NULL) {
239				close(fd);
240				return (-1);
241			}
242			value[0] = '\0';
243			goto call_function;
244		}
245
246		/* Get the current offset */
247		curpos = lseek(fd, 0, SEEK_CUR) - 1;
248		if (curpos == -1) {
249			close(fd);
250			return (-1);
251		}
252
253		/* Find the end of the value */
254		quote = 0;
255		end = 0;
256		while (r != 0 && end == 0) {
257			/* Advance to the next character if we know we can */
258			if (*p != '\"' && *p != '#' && *p != '\n' &&
259			    (!bsemicolon || *p != ';')) {
260				r = read(fd, p, 1);
261				continue;
262			}
263
264			/*
265			 * If we get this far, we've hit an end-key
266			 */
267
268			/* Get the current offset */
269			charpos = lseek(fd, 0, SEEK_CUR) - 1;
270			if (charpos == -1) {
271				close(fd);
272				return (-1);
273			}
274
275			/*
276			 * Go back so we can read the character before the key
277			 * to check if the character is escaped (which means we
278			 * should continue).
279			 */
280			error = (int)lseek(fd, -2, SEEK_CUR);
281			if (error == -3) {
282				close(fd);
283				return (-1);
284			}
285			r = read(fd, p, 1);
286
287			/*
288			 * Count how many backslashes there are (an odd number
289			 * means the key is escaped, even means otherwise).
290			 */
291			for (n = 1; *p == '\\'; n++) {
292				/* Move back another offset to read */
293				error = (int)lseek(fd, -2, SEEK_CUR);
294				if (error == -3) {
295					close(fd);
296					return (-1);
297				}
298				r = read(fd, p, 1);
299			}
300
301			/* Move offset back to the key and read it */
302			error = (int)lseek(fd, charpos, SEEK_SET);
303			if (error == (charpos - 1)) {
304				close(fd);
305				return (-1);
306			}
307			r = read(fd, p, 1);
308
309			/*
310			 * If an even number of backslashes was counted meaning
311			 * key is not escaped, we should evaluate what to do.
312			 */
313			if ((n & 1) == 1) {
314				switch (*p) {
315				case '\"':
316					/*
317				 	 * Flag current sequence of characters
318					 * to follow as being quoted (hashes
319					 * are not considered comments).
320					 */
321					quote = !quote;
322					break;
323				case '#':
324					/*
325					 * If we aren't in a quoted series, we
326					 * just hit an inline comment and have
327					 * found the end of the value.
328					 */
329					if (!quote)
330						end = 1;
331					break;
332				case '\n':
333					/*
334					 * Newline characters must always be
335					 * escaped, whether inside a quoted
336					 * series or not, otherwise they
337					 * terminate the value.
338					 */
339					end = 1;
340				case ';':
341					if (!quote && bsemicolon)
342						end = 1;
343					break;
344				}
345			} else if (*p == '\n')
346				/* Escaped newline character. increment */
347				line++;
348
349			/* Advance to the next character */
350			r = read(fd, p, 1);
351		}
352
353		/* Get the current offset */
354		charpos = lseek(fd, 0, SEEK_CUR) - 1;
355		if (charpos == -1) {
356			close(fd);
357			return (-1);
358		}
359
360		/* Get the length of the value */
361		n = (uint32_t)(charpos - curpos);
362		if (r != 0) /* more to read, but don't read ending key */
363			n--;
364
365		/* Move offset back to the beginning of the value */
366		error = (int)lseek(fd, curpos, SEEK_SET);
367		if (error == (curpos - 1)) {
368			close(fd);
369			return (-1);
370		}
371
372		/* Allocate and read the value into memory */
373		if (n > vsize) {
374			if ((value = realloc(value, n + 1)) == NULL) {
375				close(fd);
376				return (-1);
377			}
378			vsize = n;
379		}
380		r = read(fd, value, n);
381
382		/* Terminate the string */
383		value[n] = '\0';
384
385		/* Cut trailing whitespace off by termination */
386		t = value + n;
387		while (isspace(*--t))
388			*t = '\0';
389
390		/* Escape the escaped quotes (replaceall is in string_m.c) */
391		x = strcount(value, "\\\""); /* in string_m.c */
392		if (x != 0 && (n + x) > vsize) {
393			if ((value = realloc(value, n + x + 1)) == NULL) {
394				close(fd);
395				return (-1);
396			}
397			vsize = n + x;
398		}
399		if (replaceall(value, "\\\"", "\\\\\"") < 0) {
400			/* Replace operation failed for some unknown reason */
401			close(fd);
402			return (-1);
403		}
404
405		/* Remove all new line characters */
406		if (replaceall(value, "\\\n", "") < 0) {
407			/* Replace operation failed for some unknown reason */
408			close(fd);
409			return (-1);
410		}
411
412		/* Resolve escape sequences */
413		strexpand(value); /* in string_m.c */
414
415call_function:
416		/* Abort if we're seeking only assignments */
417		if (require_equals && !have_equals)
418			return (-1);
419
420		found = have_equals = 0; /* reset */
421
422		/* If there are no options defined, call unknown and loop */
423		if (options == NULL && unknown != NULL) {
424			error = unknown(NULL, line, directive, value);
425			if (error != 0) {
426				close(fd);
427				return (error);
428			}
429			continue;
430		}
431
432		/* Loop through the array looking for a match for the value */
433		for (n = 0; options[n].directive != NULL; n++) {
434			error = fnmatch(options[n].directive, directive,
435			    FNM_NOESCAPE);
436			if (error == 0) {
437				found = 1;
438				/* Call function for array index item */
439				if (options[n].action != NULL) {
440					error = options[n].action(
441					    &options[n],
442					    line, directive, value);
443					if (error != 0) {
444						close(fd);
445						return (error);
446					}
447				}
448			} else if (error != FNM_NOMATCH) {
449				/* An error has occurred */
450				close(fd);
451				return (-1);
452			}
453		}
454		if (!found && unknown != NULL) {
455			/*
456			 * No match was found for the value we read from the
457			 * file; call function designated for unknown values.
458			 */
459			error = unknown(NULL, line, directive, value);
460			if (error != 0) {
461				close(fd);
462				return (error);
463			}
464		}
465	}
466
467	close(fd);
468	return (0);
469}
470