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