qLibc
qconfig.c
Go to the documentation of this file.
1/******************************************************************************
2 * qLibc
3 *
4 * Copyright (c) 2010-2026 Seungyoung Kim.
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright notice,
11 * this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 * POSSIBILITY OF SUCH DAMAGE.
27 *****************************************************************************/
28
29/**
30 * @file qconfig.c INI-style configuration file parser.
31 */
32
33#ifndef DISABLE_QCONFIG
34
35#include <stdio.h>
36#include <stdlib.h>
37#include <stdbool.h>
38#include <string.h>
39#include <limits.h>
40#include <errno.h>
41#include "qinternal.h"
42#include "utilities/qfile.h"
43#include "utilities/qstring.h"
44#include "utilities/qsystem.h"
45#include "extensions/qconfig.h"
46
47#define _INCLUDE_DIRECTIVE "@INCLUDE "
48
49#ifndef _DOXYGEN_SKIP
50#define _VAR '$'
51#define _VAR_OPEN '{'
52#define _VAR_CLOSE '}'
53#define _VAR_CMD '!'
54#define _VAR_ENV '%'
55
56/* internal functions */
57static char *_parsestr(qlisttbl_t *tbl, const char *str);
58#endif
59
60/**
61 * Load and parse a configuration file.
62 *
63 * @param tbl qlisttbl_t pointer. If NULL, a new table is created.
64 * @param filepath path to the configuration file.
65 * @param sepchar separator used to split keys and values.
66 *
67 * @return qlisttbl_t pointer on success, or NULL if the file cannot be loaded.
68 *
69 * @code
70 * # This is the "config.conf" file.
71 * # A line that starts with # is a comment.
72 *
73 * @INCLUDE config.def => include the "config.def" file.
74 *
75 * prefix=/tmp => set a fixed value. "prefix" is the key.
76 * log=${prefix}/log => use the value of the previously defined key "prefix".
77 * user=${%USER} => use an environment variable.
78 * host=${!/bin/hostname -s} => run an external command and use its output.
79 * id=${user}@${host}
80 *
81 * # Enter the "system" section.
82 * [system] => the key "system." with value "system" is inserted.
83 * ostype=${%OSTYPE} => "system.ostype" is the key for this entry.
84 * machtype=${%MACHTYPE} => "system.machtype" is the key for this entry.
85 *
86 * # Enter the "daemon" section.
87 * [daemon]
88 * port=1234
89 * name=${user}_${host}_${system.ostype}_${system.machtype}
90 *
91 * # Leave the section and go back to the root.
92 * []
93 * rev=822
94 * @endcode
95 *
96 * @code
97 * # This is "config.def" file.
98 * prefix = /usr/local
99 * bin = ${prefix}/bin
100 * log = ${prefix}/log
101 * user = unknown
102 * host = unknown
103 * @endcode
104 *
105 * @code
106 * qlisttbl_t *tbl = qconfig_parse_file(NULL, "config.conf", '=', true);
107 * tbl->debug(tbl, stdout);
108 *
109 * [Output]
110 * bin=/usr/local/bin? (15)
111 * prefix=/tmp? (5)
112 * log=/tmp/log? (9)
113 * user=seungyoung.kim? (9)
114 * host=eng22? (6)
115 * id=seungyoung.kim@eng22? (15)
116 * system.=system? (7)
117 * system.ostype=linux? (6)
118 * system.machtype=x86_64? (7)
119 * daemon.=daemon? (7)
120 * daemon.port=1234? (5)
121 * daemon.name=seungyoung.kim_eng22_linux_x86_64? (28)
122 * rev=822? (4)
123 * @endcode
124 */
125qlisttbl_t *qconfig_parse_file(qlisttbl_t *tbl, const char *filepath,
126 char sepchar) {
127 char *str = qfile_load(filepath, NULL);
128 if (str == NULL)
129 return NULL;
130
131 // Process @INCLUDE directives.
132 char *strp = str;
133
134 while ((strp = strstr(strp, _INCLUDE_DIRECTIVE)) != NULL) {
135 if (strp == str || strp[-1] == '\n') {
136 char buf[PATH_MAX];
137
138 // Parse the file name.
139 char *tmpp;
140 for (tmpp = strp + CONST_STRLEN(_INCLUDE_DIRECTIVE);
141 *tmpp != '\n' && *tmpp != '\0'; tmpp++)
142 ;
143 int len = tmpp - (strp + CONST_STRLEN(_INCLUDE_DIRECTIVE));
144 if (len >= sizeof(buf)) {
145 DEBUG("Can't process %s directive.", _INCLUDE_DIRECTIVE);
146 free(str);
147 return NULL;
148 }
149
150 strncpy(buf, strp + CONST_STRLEN(_INCLUDE_DIRECTIVE), len);
151 buf[len] = '\0';
152 qstrtrim(buf);
153
154 // Build the full file path.
155 if (!(buf[0] == '/' || buf[0] == '\\')) {
156 char tmp[PATH_MAX];
157 char *dir = qfile_get_dir(filepath);
158 if (strlen(dir) + 1 + strlen(buf) >= sizeof(buf)) {
159 DEBUG("Can't process %s directive.", _INCLUDE_DIRECTIVE);
160 free(dir);
161 free(str);
162 return NULL;
163 }
164 snprintf(tmp, sizeof(tmp), "%s/%s", dir, buf);
165 free(dir);
166
167 strcpy(buf, tmp);
168 }
169
170 // Read the included file.
171 char *incdata;
172 if (strlen(buf) == 0 || (incdata = qfile_load(buf, NULL)) == NULL) {
173 DEBUG("Can't process '%s%s' directive.", _INCLUDE_DIRECTIVE,
174 buf);
175 free(str);
176 return NULL;
177 }
178
179 // Replace the directive with the file contents.
180 strncpy(buf, strp, CONST_STRLEN(_INCLUDE_DIRECTIVE) + len);
181 buf[CONST_STRLEN(_INCLUDE_DIRECTIVE) + len] = '\0';
182 strp = qstrreplace("sn", str, buf, incdata);
183 free(incdata);
184 free(str);
185 str = strp;
186 } else {
187 strp += CONST_STRLEN(_INCLUDE_DIRECTIVE);
188 }
189 }
190
191 // Parse the final string.
192 tbl = qconfig_parse_str(tbl, str, sepchar);
193 free(str);
194
195 return tbl;
196}
197
198/**
199 * Parse a configuration string.
200 *
201 * @param tbl qlisttbl_t pointer. If NULL, a new table is created.
202 * @param str string that contains key/value pairs.
203 * @param sepchar separator used to split keys and values.
204 *
205 * @return qlisttbl_t pointer on success, or NULL on failure.
206 *
207 * @see qconfig_parse_file
208 *
209 * @code
210 * qlisttbl_t *tbl;
211 * tbl = qconfig_parse_str(NULL, "key = value\nhello = world", '=');
212 * @endcode
213 */
214qlisttbl_t *qconfig_parse_str(qlisttbl_t *tbl, const char *str, char sepchar) {
215 if (str == NULL)
216 return NULL;
217
218 if (tbl == NULL) {
219 tbl = qlisttbl(0);
220 if (tbl == NULL)
221 return NULL;
222 }
223
224 char *section = NULL;
225 char *org, *buf, *offset;
226 for (org = buf = offset = strdup(str); *offset != '\0';) {
227 // Read one line into buf.
228 for (buf = offset; *offset != '\n' && *offset != '\0'; offset++)
229 ;
230 if (*offset != '\0') {
231 *offset = '\0';
232 offset++;
233 }
234 qstrtrim(buf);
235
236 // Skip blank lines and comments.
237 if ((buf[0] == '#') || (buf[0] == '\0'))
238 continue;
239
240 // Parse a section header.
241 if ((buf[0] == '[') && (buf[strlen(buf) - 1] == ']')) {
242 // Extract the section name.
243 if (section != NULL)
244 free(section);
245 section = strdup(buf + 1);
246 section[strlen(section) - 1] = '\0';
247 qstrtrim(section);
248
249 // Clear the section if the name is empty, such as [].
250 if (section[0] == '\0') {
251 free(section);
252 section = NULL;
253 continue;
254 }
255
256 // Store the section name as "section.=section".
257 sprintf(buf, "%c%s", sepchar, section);
258 }
259
260 // Parse and store the entry.
261 char *value = strdup(buf);
262 char *name = _q_makeword(value, sepchar);
263 qstrtrim(value);
264 qstrtrim(name);
265
266 // Add the section name as a prefix.
267 if (section != NULL) {
268 char *newname = qstrdupf("%s.%s", section, name);
269 free(name);
270 name = newname;
271 }
272
273 // Resolve variables in the value.
274 char *newvalue = _parsestr(tbl, value);
275 if (newvalue != NULL) {
276 tbl->putstr(tbl, name, newvalue);
277 free(newvalue);
278 }
279
280 free(name);
281 free(value);
282 }
283 free(org);
284 if (section != NULL)
285 free(section);
286
287 return tbl;
288}
289
290#ifndef _DOXYGEN_SKIP
291
292/**
293 * (qlisttbl_t*)->parsestr(): Parse a string and replace variables with
294 * values from this table.
295 *
296 * @param tbl qlisttbl container pointer.
297 * @param str string value that may contain variables like ${...}
298 *
299 * @return allocated string on success, otherwise NULL.
300 * @retval errno will be set on error.
301 * - EINVAL : Invalid argument.
302 *
303 * @code
304 * ${key_name} - replace with the matching value from this table.
305 * ${!system_command} - run an external command and use its output.
306 * ${%PATH} - get an environment variable.
307 * @endcode
308 *
309 * @code
310 * --[tbl Table]------------------------
311 * NAME = qLibc
312 * -------------------------------------
313 *
314 * char *str = _parsestr(tbl, "${NAME}, ${%HOME}, ${!date -u}");
315 * if (str != NULL) {
316 * printf("%s\n", str);
317 * free(str);
318 * }
319 *
320 * [Output]
321 * qLibc, /home/qlibc, Wed Nov 24 00:30:58 UTC 2010
322 * @endcode
323 */
324static char *_parsestr(qlisttbl_t *tbl, const char *str) {
325 if (str == NULL) {
326 errno = EINVAL;
327 return NULL;
328 }
329
330 bool loop;
331 char *value = strdup(str);
332 do {
333 loop = false;
334
335 // Find the next ${ token.
336 char *s, *e;
337 int openedbrakets;
338 for (s = value; *s != '\0'; s++) {
339 if (!(*s == _VAR && *(s + 1) == _VAR_OPEN))
340 continue;
341
342 // Found ${. Now look for the matching }.
343 openedbrakets = 1; // Number of open brackets.
344 for (e = s + 2; *e != '\0'; e++) {
345 if (*e == _VAR && *(e + 1) == _VAR_OPEN) { // Found a nested ${
346 // e is always greater than s, so this cannot underflow.
347 s = e - 1;
348 break;
349 } else if (*e == _VAR_OPEN)
350 openedbrakets++;
351 else if (*e == _VAR_CLOSE)
352 openedbrakets--;
353 else
354 continue;
355
356 if (openedbrakets == 0)
357 break;
358 }
359 if (*e == '\0')
360 break; // Brackets do not match.
361 if (openedbrakets > 0)
362 continue; // A nested ${ was found.
363
364 // Copy the text between ${ and }.
365 int varlen = e - s - 2; // Length between ${ and }.
366 char *varstr = (char *) malloc(varlen + 3 + 1);
367 if (varstr == NULL)
368 continue;
369 strncpy(varstr, s + 2, varlen);
370 varstr[varlen] = '\0';
371
372 // Resolve the replacement string.
373 char *newstr = NULL;
374 switch (varstr[0]) {
375 case _VAR_CMD: {
376 if (varlen - 1 == 0) {
377 newstr = strdup("");
378 break;
379 }
380 if ((newstr = qstrtrim(qsyscmd(varstr + 1))) == NULL) {
381 newstr = strdup("");
382 }
383 break;
384 }
385 case _VAR_ENV: {
386 if (varlen - 1 == 0) {
387 newstr = strdup("");
388 break;
389 }
390 newstr = strdup(qgetenv(varstr + 1, ""));
391 break;
392 }
393 default: {
394 if (varlen == 0) {
395 newstr = strdup("");
396 break;
397 }
398 if ((newstr = tbl->getstr(tbl, varstr, true)) == NULL) {
399 s = e; // No matching value was found.
400 continue;
401 }
402 break;
403 }
404 }
405
406 // Replace the original token.
407 strncpy(varstr, s, varlen + 3); // Copy ${str}.
408 varstr[varlen + 3] = '\0';
409
410 s = qstrreplace("sn", value, varstr, newstr);
411 free(newstr);
412 free(varstr);
413 free(value);
414 value = s;
415
416 loop = true;
417 break;
418 }
419 } while (loop == true);
420
421 return value;
422}
423
424#endif /* _DOXYGEN_SKIP */
425
426#endif /* DISABLE_QCONFIG */
427
qlisttbl_t * qconfig_parse_file(qlisttbl_t *tbl, const char *filepath, char sepchar)
Load and parse a configuration file.
Definition qconfig.c:125
qlisttbl_t * qconfig_parse_str(qlisttbl_t *tbl, const char *str, char sepchar)
Parse a configuration string.
Definition qconfig.c:214
void * qfile_load(const char *filepath, size_t *nbytes)
Load a file into memory.
Definition qfile.c:159
char * qfile_get_dir(const char *filepath)
Get the directory part of a path.
Definition qfile.c:372
qlisttbl_t * qlisttbl(int options)
Create a new Q_LIST linked-list container.
Definition qlisttbl.c:149
char * qstrtrim(char *str)
Remove whitespace, including CR and LF, from both ends of a string.
Definition qstring.c:55
char * qstrdupf(const char *format,...)
Duplicate a formatted string.
Definition qstring.c:336
char * qstrreplace(const char *mode, char *srcstr, const char *tokstr, const char *word)
Replace tokens or strings in a source string using the given mode.
Definition qstring.c:209
const char * qgetenv(const char *envname, const char *defstr)
Get a system environment variable.
Definition qsystem.c:54
char * qsyscmd(const char *cmd)
Run an external command and return its output.
Definition qsystem.c:71