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