Format string bug in WMenuParserError
[wmaker-crm.git] / WINGs / menuparser.c
blob96555cdf99d878bb8fd0ce70c63c95e51cec4778
1 /*
2 * Window Maker window manager
4 * Copyright (c) 2012 Christophe Curis
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "wconfig.h"
22 #include <stdio.h>
23 #include <string.h>
24 #include <ctype.h>
25 #include <stdarg.h>
27 #include <WINGs/WUtil.h>
29 #include "menuparser.h"
31 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
32 const char *include_default_paths);
33 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros);
34 static void menu_parser_get_directive(WMenuParser parser);
35 static Bool menu_parser_include_file(WMenuParser parser);
36 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists);
37 static void menu_parser_condition_else(WMenuParser parser);
38 static void menu_parser_condition_end(WMenuParser parser);
41 /***** Constructor and Destructor for the Menu Parser object *****/
42 WMenuParser WMenuParserCreate(const char *file_name, void *file,
43 const char *include_default_paths)
45 WMenuParser parser;
47 parser = menu_parser_create_new(file_name, file, include_default_paths);
48 menu_parser_register_preset_macros(parser);
49 return parser;
52 void WMenuParserDelete(WMenuParser parser)
54 if (parser->include_file) {
55 /* Trick: the top parser's data are not wmalloc'd, we point on the
56 provided data so we do not wfree it; however for include files
57 we did wmalloc them.
58 This code should not be used as the wfree is done when we reach
59 the end of an include file; however this may not happen when an
60 early exit occurs (typically when 'readMenuFile' does not find
61 its expected command). */
62 fclose(parser->include_file->file_handle);
63 wfree((char *) parser->include_file->file_name);
64 WMenuParserDelete(parser->include_file);
67 if (parser->macros)
68 menu_parser_free_macros(parser);
70 wfree(parser);
73 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
74 const char *include_default_paths)
76 WMenuParser parser;
78 parser = wmalloc(sizeof(*parser));
79 parser->include_default_paths = include_default_paths;
80 parser->file_name = file_name;
81 parser->file_handle = file;
82 parser->rd = parser->line_buffer;
84 return parser;
87 /***** To report helpfull messages to user *****/
88 const char *WMenuParserGetFilename(WMenuParser parser)
90 return parser->file_name;
93 void WMenuParserError(WMenuParser parser, const char *msg, ...)
95 char buf[MAXLINE];
96 va_list args;
97 WMenuParser parent;
99 while (parser->include_file)
100 parser = parser->include_file;
102 va_start(args, msg);
103 vsnprintf(buf, sizeof(buf), msg, args);
104 va_end(args);
105 __wmessage("WMenuParser", parser->file_name, parser->line_number, WMESSAGE_TYPE_WARNING, "%s", buf);
107 for (parent = parser->parent_file; parent != NULL; parent = parent->parent_file)
108 __wmessage("WMenuParser", parser->file_name, parser->line_number, WMESSAGE_TYPE_WARNING,
109 _(" included from file \"%s\" at line %d"),
110 parent->file_name, parent->line_number);
113 /***** Read one line from file and split content *****/
114 /* The function returns False when the end of file is reached */
115 Bool WMenuParserGetLine(WMenuParser top_parser, char **title, char **command, char **parameter, char **shortcut)
117 WMenuParser cur_parser;
118 enum { GET_TITLE, GET_COMMAND, GET_PARAMETERS, GET_SHORTCUT } scanmode;
119 char *token;
120 char lineparam[MAXLINE];
121 char *params = NULL;
123 lineparam[0] = '\0';
124 *title = NULL;
125 *command = NULL;
126 *parameter = NULL;
127 *shortcut = NULL;
128 scanmode = GET_TITLE;
130 read_next_line_with_filechange:
131 cur_parser = top_parser;
132 while (cur_parser->include_file)
133 cur_parser = cur_parser->include_file;
135 read_next_line:
136 if (fgets(cur_parser->line_buffer, sizeof(cur_parser->line_buffer), cur_parser->file_handle) == NULL) {
137 if (cur_parser->cond.depth > 0) {
138 int i;
140 for (i = 0; i < cur_parser->cond.depth; i++)
141 WMenuParserError(cur_parser, _("missing #endif to match #%s at line %d"),
142 cur_parser->cond.stack[i].name, cur_parser->cond.stack[i].line);
145 if (cur_parser->parent_file == NULL)
146 /* Not inside an included file -> we have reached the end */
147 return False;
149 /* We have only reached the end of an included file -> go back to calling file */
150 fclose(cur_parser->file_handle);
151 wfree((char *) cur_parser->file_name);
152 cur_parser = cur_parser->parent_file;
153 wfree(cur_parser->include_file);
154 cur_parser->include_file = NULL;
155 goto read_next_line_with_filechange;
157 cur_parser->line_number++;
158 cur_parser->rd = cur_parser->line_buffer;
160 for (;;) {
161 if (!menu_parser_skip_spaces_and_comments(cur_parser)) {
162 /* We reached the end of line */
163 if (scanmode == GET_TITLE)
164 goto read_next_line; // Empty line -> skip
165 else
166 break; // Finished reading current line -> return it to caller
168 if ((scanmode == GET_TITLE) && (*cur_parser->rd == '#')) {
169 cur_parser->rd++;
170 menu_parser_get_directive(cur_parser);
171 goto read_next_line_with_filechange;
173 if (cur_parser->cond.stack[0].skip)
174 goto read_next_line;
176 /* Found a word */
177 token = menu_parser_isolate_token(cur_parser, top_parser->macros);
178 switch (scanmode) {
179 case GET_TITLE:
180 *title = token;
181 scanmode = GET_COMMAND;
182 break;
184 case GET_COMMAND:
185 if (strcmp(token, "SHORTCUT") == 0) {
186 scanmode = GET_SHORTCUT;
187 wfree(token);
188 } else {
189 *command = token;
190 scanmode = GET_PARAMETERS;
192 break;
194 case GET_SHORTCUT:
195 if (*shortcut != NULL) {
196 WMenuParserError(top_parser, _("multiple SHORTCUT definition not valid") );
197 wfree(*shortcut);
199 *shortcut = token;
200 scanmode = GET_COMMAND;
201 break;
203 case GET_PARAMETERS:
205 char *src;
207 if (params == NULL) {
208 params = lineparam;
209 } else {
210 if ((params - lineparam) < sizeof(lineparam)-1)
211 *params++ = ' ';
213 src = token;
214 while ((params - lineparam) < sizeof(lineparam)-1)
215 if ( (*params = *src++) == '\0')
216 break;
217 else
218 params++;
219 wfree(token);
221 break;
225 if (params != NULL) {
226 lineparam[sizeof(lineparam) - 1] = '\0';
227 *parameter = wstrdup(lineparam);
230 return True;
233 /* Return False when there's nothing left on the line,
234 otherwise increment parser's pointer to next token */
235 Bool menu_parser_skip_spaces_and_comments(WMenuParser parser)
237 for (;;) {
238 while (isspace(*parser->rd))
239 parser->rd++;
241 if (*parser->rd == '\0')
242 return False; // Found the end of current line
244 else if ((parser->rd[0] == '\\') &&
245 (parser->rd[1] == '\n') &&
246 (parser->rd[2] == '\0')) {
247 // Means that the current line is expected to be continued on next line
248 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
249 WMenuParserError(parser, _("premature end of file while expecting a new line after '\\'") );
250 return False;
252 parser->line_number++;
253 parser->rd = parser->line_buffer;
255 } else if (parser->rd[0] == '/') {
256 if (parser->rd[1] == '/') // Single line C comment
257 return False; // Won't find anything more on this line
258 if (parser->rd[1] == '*') {
259 int start_line;
261 start_line = parser->line_number;
262 parser->rd += 2;
263 for (;;) {
264 /* Search end-of-comment marker */
265 while (*parser->rd != '\0') {
266 if (parser->rd[0] == '*')
267 if (parser->rd[1] == '/')
268 goto found_end_of_comment;
269 parser->rd++;
272 /* Marker not found -> load next line */
273 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
274 WMenuParserError(parser, _("reached end of file while searching '*/' for comment started at line %d"), start_line);
275 return False;
277 parser->line_number++;
278 parser->rd = parser->line_buffer;
281 found_end_of_comment:
282 parser->rd += 2; // Skip closing mark
283 continue; // Because there may be spaces after the comment
285 return True; // the '/' was not a comment, treat it as user data
286 } else
287 return True; // Found some data
291 /* read a token (non-spaces suite of characters)
292 the result os wmalloc's, so it needs to be free'd */
293 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros)
295 char *start;
296 char *token;
297 int limit = MAX_NESTED_MACROS;
299 start = parser->rd;
300 restart_token_split:
302 while (*parser->rd != '\0')
303 if (isspace(*parser->rd))
304 break;
305 else if ((parser->rd[0] == '/') &&
306 ((parser->rd[1] == '*') || (parser->rd[1] == '/')))
307 break;
308 else if ((parser->rd[0] == '\\') && (parser->rd[1] == '\n'))
309 break;
310 else if ((*parser->rd == '"' ) || (*parser->rd == '\'')) {
311 char eot = *parser->rd++;
312 while ((*parser->rd != '\0') && (*parser->rd != '\n'))
313 if (*parser->rd++ == eot)
314 goto found_end_quote;
315 WMenuParserError(parser, _("missing closing quote or double-quote before end-of-line") );
316 found_end_quote:
318 } else if (isnamechr(*parser->rd)) {
319 WParserMacro *macro;
320 char *start_macro;
322 start_macro = parser->rd;
323 while (isnamechr(*parser->rd))
324 parser->rd++;
326 macro = menu_parser_find_macro(parser, start_macro);
327 if (macro != NULL) {
328 char *expand_there;
330 /* Copy the chars before the macro to the beginning of the buffer to
331 leave as much room as possible for expansion */
332 expand_there = parser->line_buffer;
333 if (start != parser->line_buffer)
334 while (start < start_macro)
335 *expand_there++ = *start++;
336 start = parser->line_buffer;
338 /* Macro expansion will take care to keep the rest of the line after
339 the macro to the end of the line for future token extraction */
340 menu_parser_expand_macro(parser, macro, expand_there,
341 sizeof(parser->line_buffer) - (start - parser->line_buffer) );
343 /* Restart parsing to allow expansion of sub macro calls */
344 parser->rd = expand_there;
345 if (limit-- > 0)
346 goto restart_token_split;
347 WMenuParserError(parser, _("too many nested macro expansion, breaking loop") );
348 while (isnamechr(*parser->rd))
349 parser->rd++;
350 break;
352 // else: the text was passed over so it will be counted in the token from 'start'
353 } else
354 parser->rd++;
356 token = wmalloc(parser->rd - start + 1);
357 strncpy(token, start, parser->rd - start);
358 token[parser->rd - start] = '\0';
360 return token;
363 /***** Processing of special # directives *****/
364 static void menu_parser_get_directive(WMenuParser parser)
366 char *command;
368 /* Isolate the command */
369 while (isspace(*parser->rd))
370 parser->rd++;
371 command = parser->rd;
372 while (*parser->rd)
373 if (isspace(*parser->rd)) {
374 *parser->rd++ = '\0';
375 break;
376 } else parser->rd++;
378 if (strcmp(command, "include") == 0) {
379 if (!menu_parser_include_file(parser)) return;
381 } else if (strcmp(command, "define") == 0) {
382 menu_parser_define_macro(parser);
384 } else if (strcmp(command, "ifdef") == 0) {
385 menu_parser_condition_ifmacro(parser, 1);
387 } else if (strcmp(command, "ifndef") == 0) {
388 menu_parser_condition_ifmacro(parser, 0);
390 } else if (strcmp(command, "else") == 0) {
391 menu_parser_condition_else(parser);
393 } else if (strcmp(command, "endif") == 0) {
394 menu_parser_condition_end(parser);
396 } else {
397 WMenuParserError(parser, _("unknow directive '#%s'"), command);
398 return;
401 if (menu_parser_skip_spaces_and_comments(parser))
402 WMenuParserError(parser, _("extra text after '#' command is ignored: \"%.16s...\""),
403 parser->rd);
406 /* Extract the file name, search for it in known directories
407 and create a sub-parser to handle it.
408 Returns False if the file could not be found */
409 static Bool menu_parser_include_file(WMenuParser parser)
411 char buffer[MAXLINE];
412 char *req_filename, *fullfilename, *p;
413 char eot;
414 FILE *fh;
416 if (!menu_parser_skip_spaces_and_comments(parser)) {
417 WMenuParserError(parser, _("no file name found for #include") );
418 return False;
420 switch (*parser->rd++) {
421 case '<': eot = '>'; break;
422 case '"': eot = '"'; break;
423 default:
424 WMenuParserError(parser, _("file name must be enclosed in brackets or double-quotes for #define") );
425 return False;
427 req_filename = parser->rd;
428 while (*parser->rd)
429 if (*parser->rd == eot) {
430 *parser->rd++ = '\0';
431 goto found_end_define_fname;
432 } else parser->rd++;
433 WMenuParserError(parser, _("missing closing '%c' in filename specification"), eot);
434 return False;
435 found_end_define_fname:
437 /* If we're inside a #if sequence, we abort now, but not sooner in
438 order to keep the syntax check */
439 if (parser->cond.stack[0].skip)
440 return False;
442 { /* Check we are not nesting too many includes */
443 WMenuParser p;
444 int count;
446 count = 0;
447 for (p = parser; p->parent_file; p = p->parent_file)
448 count++;
449 if (count > MAX_NESTED_INCLUDES) {
450 WMenuParserError(parser, _("too many nested includes") );
451 return False;
455 /* Absolute paths */
456 fullfilename = req_filename;
457 if (req_filename[0] != '/') {
458 /* Search first in the same directory as the current file */
459 p = strrchr(parser->file_name, '/');
460 if (p != NULL) {
461 int len;
463 len = p - parser->file_name + 1;
464 if (len > sizeof(buffer) - 1) len = sizeof(buffer) - 1;
465 strncpy(buffer, parser->file_name, len);
466 strncpy(buffer+len, req_filename, sizeof(buffer) - len - 1);
467 buffer[sizeof(buffer) - 1] = '\0';
468 fullfilename = buffer;
471 fh = fopen(fullfilename, "rb");
473 /* Not found? Search in wmaker's known places */
474 if (fh == NULL) {
475 if (req_filename[0] != '/') {
476 const char *src;
478 fullfilename = buffer;
479 src = parser->include_default_paths;
480 while (*src != '\0') {
481 p = buffer;
482 if (*src == '~') {
483 char *home = wgethomedir();
484 while (*home != '\0')
485 *p++ = *home++;
486 src++;
488 while ((*src != '\0') && (*src != ':'))
489 *p++ = *src++;
490 *p++ = '/';
491 strncpy(p, req_filename, sizeof(buffer) - (p - buffer - 1));
492 buffer[sizeof(buffer) - 1] = '\0';
494 fh = fopen(fullfilename, "rb");
495 if (fh != NULL) goto found_valid_file;
498 WMenuParserError(parser, _("could not find file \"%s\" for include"), req_filename);
499 return False;
502 /* Found the file, make it our new source */
503 found_valid_file:
504 parser->include_file = menu_parser_create_new(wstrdup(req_filename), fh, parser->include_default_paths);
505 parser->include_file->parent_file = parser;
506 return True;
509 /* Check wether a macro exists or not, and marks the parser to ignore the
510 following data accordingly */
511 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists)
513 WParserMacro *macro;
514 int idx;
515 const char *cmd_name, *macro_name;
517 cmd_name = check_exists?"ifdef":"ifndef";
518 if (!menu_parser_skip_spaces_and_comments(parser)) {
519 WMenuParserError(parser, _("missing macro name argument to #%s"), cmd_name);
520 return;
523 /* jump to end of provided name for later checks that no extra stuff is following */
524 macro_name = parser->rd;
525 while (isnamechr(*parser->rd))
526 parser->rd++;
528 /* Add this condition to the stack of conditions */
529 if (parser->cond.depth >= sizeof(parser->cond.stack) / sizeof(parser->cond.stack[0])) {
530 WMenuParserError(parser, _("too many nested #if sequences") );
531 return;
533 for (idx = parser->cond.depth - 1; idx >= 0; idx--)
534 parser->cond.stack[idx + 1] = parser->cond.stack[idx];
535 parser->cond.depth++;
537 if (parser->cond.stack[1].skip)
538 parser->cond.stack[0].skip = True;
539 else {
540 macro = menu_parser_find_macro(parser, macro_name);
541 parser->cond.stack[0].skip =
542 ((check_exists) && (macro == NULL)) ||
543 ((!check_exists) && (macro != NULL)) ;
545 strcpy(parser->cond.stack[0].name, cmd_name);
546 parser->cond.stack[0].line = parser->line_number;
549 /* Swap the 'data ignore' flag because a #else condition was found */
550 static void menu_parser_condition_else(WMenuParser parser)
552 if (parser->cond.depth <= 0) {
553 WMenuParserError(parser, _("found #%s but have no matching #if"), "else" );
554 return;
556 if ((parser->cond.depth > 1) && (parser->cond.stack[1].skip))
557 // The containing #if is false, so we continue skipping anyway
558 parser->cond.stack[0].skip = True;
559 else
560 parser->cond.stack[0].skip = !parser->cond.stack[0].skip;
563 /* Closes the current conditional, removing it from the stack */
564 static void menu_parser_condition_end(WMenuParser parser)
566 int idx;
568 if (parser->cond.depth <= 0) {
569 WMenuParserError(parser, _("found #%s but have no matching #if"), "endif" );
570 return;
573 if (--parser->cond.depth > 0)
574 for (idx = 0; idx < parser->cond.depth; idx++)
575 parser->cond.stack[idx] = parser->cond.stack[idx + 1];
576 else
577 parser->cond.stack[0].skip = False;