1 /* inih -- simple .INI file parser
2 
3 SPDX-License-Identifier: BSD-3-Clause
4 
5 Copyright (C) 2009-2020, Ben Hoyt
6 
7 inih is released under the New BSD license (see LICENSE.txt). Go to the project
8 home page for more info:
9 
10 https://github.com/benhoyt/inih
11 
12 */
13 
14 #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
15 #define _CRT_SECURE_NO_WARNINGS
16 #endif
17 
18 #include <stdio.h>
19 #include <ctype.h>
20 #include <string.h>
21 
22 #include "ini.h"
23 
24 #if !INI_USE_STACK
25 #if INI_CUSTOM_ALLOCATOR
26 #include <stddef.h>
27 void* ini_malloc(size_t size);
28 void ini_free(void* ptr);
29 void* ini_realloc(void* ptr, size_t size);
30 #else
31 #include <stdlib.h>
32 #define ini_malloc malloc
33 #define ini_free free
34 #define ini_realloc realloc
35 #endif
36 #endif
37 
38 #define MAX_SECTION 50
39 #define MAX_NAME 50
40 
41 /* Used by ini_parse_string() to keep track of string parsing state. */
42 typedef struct {
43     const char* ptr;
44     size_t num_left;
45 } ini_parse_string_ctx;
46 
47 /* Strip whitespace chars off end of given string, in place. Return s. */
rstrip(char * s)48 static char* rstrip(char* s)
49 {
50     char* p = s + strlen(s);
51     while (p > s && isspace((unsigned char)(*--p)))
52         *p = '\0';
53     return s;
54 }
55 
56 /* Return pointer to first non-whitespace char in given string. */
lskip(const char * s)57 static char* lskip(const char* s)
58 {
59     while (*s && isspace((unsigned char)(*s)))
60         s++;
61     return (char*)s;
62 }
63 
64 /* Return pointer to first char (of chars) or inline comment in given string,
65    or pointer to NUL at end of string if neither found. Inline comment must
66    be prefixed by a whitespace character to register as a comment. */
find_chars_or_comment(const char * s,const char * chars)67 static char* find_chars_or_comment(const char* s, const char* chars)
68 {
69 #if INI_ALLOW_INLINE_COMMENTS
70     int was_space = 0;
71     while (*s && (!chars || !strchr(chars, *s)) &&
72            !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
73         was_space = isspace((unsigned char)(*s));
74         s++;
75     }
76 #else
77     while (*s && (!chars || !strchr(chars, *s))) {
78         s++;
79     }
80 #endif
81     return (char*)s;
82 }
83 
84 /* Similar to strncpy, but ensures dest (size bytes) is
85    NUL-terminated, and doesn't pad with NULs. */
strncpy0(char * dest,const char * src,size_t size)86 static char* strncpy0(char* dest, const char* src, size_t size)
87 {
88     /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */
89     size_t i;
90     for (i = 0; i < size - 1 && src[i]; i++)
91         dest[i] = src[i];
92     dest[i] = '\0';
93     return dest;
94 }
95 
96 /* See documentation in header file. */
ini_parse_stream(ini_reader reader,void * stream,ini_handler handler,void * user)97 int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
98                      void* user)
99 {
100     /* Uses a fair bit of stack (use heap instead if you need to) */
101 #if INI_USE_STACK
102     char line[INI_MAX_LINE];
103     int max_line = INI_MAX_LINE;
104 #else
105     char* line;
106     size_t max_line = INI_INITIAL_ALLOC;
107 #endif
108 #if INI_ALLOW_REALLOC && !INI_USE_STACK
109     char* new_line;
110     size_t offset;
111 #endif
112     char section[MAX_SECTION] = "";
113     char prev_name[MAX_NAME] = "";
114 
115     char* start;
116     char* end;
117     char* name;
118     char* value;
119     int lineno = 0;
120     int error = 0;
121 
122 #if !INI_USE_STACK
123     line = (char*)ini_malloc(INI_INITIAL_ALLOC);
124     if (!line) {
125         return -2;
126     }
127 #endif
128 
129 #if INI_HANDLER_LINENO
130 #define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
131 #else
132 #define HANDLER(u, s, n, v) handler(u, s, n, v)
133 #endif
134 
135     /* Scan through stream line by line */
136     while (reader(line, (int)max_line, stream) != NULL) {
137 #if INI_ALLOW_REALLOC && !INI_USE_STACK
138         offset = strlen(line);
139         while (offset == max_line - 1 && line[offset - 1] != '\n') {
140             max_line *= 2;
141             if (max_line > INI_MAX_LINE)
142                 max_line = INI_MAX_LINE;
143             new_line = ini_realloc(line, max_line);
144             if (!new_line) {
145                 ini_free(line);
146                 return -2;
147             }
148             line = new_line;
149             if (reader(line + offset, (int)(max_line - offset), stream) == NULL)
150                 break;
151             if (max_line >= INI_MAX_LINE)
152                 break;
153             offset += strlen(line + offset);
154         }
155 #endif
156 
157         lineno++;
158 
159         start = line;
160 #if INI_ALLOW_BOM
161         if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
162                            (unsigned char)start[1] == 0xBB &&
163                            (unsigned char)start[2] == 0xBF) {
164             start += 3;
165         }
166 #endif
167         start = lskip(rstrip(start));
168 
169         if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
170             /* Start-of-line comment */
171         }
172 #if INI_ALLOW_MULTILINE
173         else if (*prev_name && *start && start > line) {
174             /* Non-blank line with leading whitespace, treat as continuation
175                of previous name's value (as per Python configparser). */
176             if (!HANDLER(user, section, prev_name, start) && !error)
177                 error = lineno;
178         }
179 #endif
180         else if (*start == '[') {
181             /* A "[section]" line */
182             end = find_chars_or_comment(start + 1, "]");
183             if (*end == ']') {
184                 *end = '\0';
185                 strncpy0(section, start + 1, sizeof(section));
186                 *prev_name = '\0';
187 #if INI_CALL_HANDLER_ON_NEW_SECTION
188                 if (!HANDLER(user, section, NULL, NULL) && !error)
189                     error = lineno;
190 #endif
191             }
192             else if (!error) {
193                 /* No ']' found on section line */
194                 error = lineno;
195             }
196         }
197         else if (*start) {
198             /* Not a comment, must be a name[=:]value pair */
199             end = find_chars_or_comment(start, "=:");
200             if (*end == '=' || *end == ':') {
201                 *end = '\0';
202                 name = rstrip(start);
203                 value = end + 1;
204 #if INI_ALLOW_INLINE_COMMENTS
205                 end = find_chars_or_comment(value, NULL);
206                 if (*end)
207                     *end = '\0';
208 #endif
209                 value = lskip(value);
210                 rstrip(value);
211 
212                 /* Valid name[=:]value pair found, call handler */
213                 strncpy0(prev_name, name, sizeof(prev_name));
214                 if (!HANDLER(user, section, name, value) && !error)
215                     error = lineno;
216             }
217             else if (!error) {
218                 /* No '=' or ':' found on name[=:]value line */
219 #if INI_ALLOW_NO_VALUE
220                 *end = '\0';
221                 name = rstrip(start);
222                 if (!HANDLER(user, section, name, NULL) && !error)
223                     error = lineno;
224 #else
225                 error = lineno;
226 #endif
227             }
228         }
229 
230 #if INI_STOP_ON_FIRST_ERROR
231         if (error)
232             break;
233 #endif
234     }
235 
236 #if !INI_USE_STACK
237     ini_free(line);
238 #endif
239 
240     return error;
241 }
242 
243 /* See documentation in header file. */
ini_parse_file(FILE * file,ini_handler handler,void * user)244 int ini_parse_file(FILE* file, ini_handler handler, void* user)
245 {
246     return ini_parse_stream((ini_reader)fgets, file, handler, user);
247 }
248 
249 /* See documentation in header file. */
ini_parse(const char * filename,ini_handler handler,void * user)250 int ini_parse(const char* filename, ini_handler handler, void* user)
251 {
252     FILE* file;
253     int error;
254 
255     file = fopen(filename, "r");
256     if (!file)
257         return -1;
258     error = ini_parse_file(file, handler, user);
259     fclose(file);
260     return error;
261 }
262 
263 /* An ini_reader function to read the next line from a string buffer. This
264    is the fgets() equivalent used by ini_parse_string(). */
ini_reader_string(char * str,int num,void * stream)265 static char* ini_reader_string(char* str, int num, void* stream) {
266     ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;
267     const char* ctx_ptr = ctx->ptr;
268     size_t ctx_num_left = ctx->num_left;
269     char* strp = str;
270     char c;
271 
272     if (ctx_num_left == 0 || num < 2)
273         return NULL;
274 
275     while (num > 1 && ctx_num_left != 0) {
276         c = *ctx_ptr++;
277         ctx_num_left--;
278         *strp++ = c;
279         if (c == '\n')
280             break;
281         num--;
282     }
283 
284     *strp = '\0';
285     ctx->ptr = ctx_ptr;
286     ctx->num_left = ctx_num_left;
287     return str;
288 }
289 
290 /* See documentation in header file. */
ini_parse_string(const char * string,ini_handler handler,void * user)291 int ini_parse_string(const char* string, ini_handler handler, void* user) {
292     ini_parse_string_ctx ctx;
293 
294     ctx.ptr = string;
295     ctx.num_left = strlen(string);
296     return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,
297                             user);
298 }
299