xref: /freebsd/lib/libfigpar/figpar.c (revision ff0ba87247820afbdfdc1b307c803f7923d0e4d3)
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$");
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 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  */
53 struct fp_config *
54 get_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 */
86 int
87 parse_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 
415 call_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