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