]> icculus.org git repositories - dana/openbox.git/blob - openbox/action_parser.c
Make warnings about parse problems in .desktop files "debug" messages. Most people...
[dana/openbox.git] / openbox / action_parser.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    action_parser.c for the Openbox window manager
4    Copyright (c) 2011        Dana Jansens
5
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.
10
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.
15
16    See the COPYING file for a copy of the GNU General Public License.
17 */
18
19 #include "action_parser.h"
20 #include "action.h"
21 #include "action_filter.h"
22 #include "action_list.h"
23 #include "config_value.h"
24 #include "gettext.h"
25
26 #ifdef HAVE_STRING_H
27 #  include <string.h>
28 #endif
29
30 struct _ObActionList;
31 struct _ObActionListTest;
32
33 #define SKIP " \t"
34 #define IDENTIFIER_FIRST G_CSET_a_2_z G_CSET_A_2_Z G_CSET_DIGITS "-_"
35 #define IDENTIFIER_NTH IDENTIFIER_FIRST G_CSET_LATINS G_CSET_LATINC
36 #define ESCAPE_SEQS "\"()"
37
38 struct _ObActionList* parse_list(ObActionParser *p,
39                                  GTokenType end,
40                                  gboolean *e);
41 struct _ObActionList* parse_action(ObActionParser *p, gboolean *e);
42 struct _ObActionList* parse_filter(ObActionParser *p, gboolean *e);
43 struct _ObActionListTest* parse_filter_test(ObActionParser *p, gboolean *e);
44 struct _ObConfigValue* parse_value(ObActionParser *p,
45                                    gboolean allow_actions,
46                                    gboolean *e);
47 gchar* parse_string(ObActionParser *p, guchar end, gboolean *e);
48 static void display_error(GScanner *scanner, gchar *message, gboolean error);
49
50 struct _ObActionParser
51 {
52     gint ref;
53     GScanner *scan;
54
55     ObActionParserErrorFunc on_error;
56     gpointer on_error_user_data;
57 };
58
59 static ObActionParserError last_error;
60
61 ObActionParser* action_parser_new(const gchar *source)
62 {
63     ObActionParser *p;
64     GScannerConfig config = {
65         .cset_skip_characters = SKIP,
66         .cset_identifier_first =  IDENTIFIER_FIRST,
67         .cset_identifier_nth = IDENTIFIER_NTH,
68         .cpair_comment_single = "#\n",
69         .case_sensitive = FALSE,
70         .skip_comment_multi = TRUE,
71         .skip_comment_single = TRUE,
72         .scan_comment_multi = FALSE,
73         .scan_identifier = TRUE,
74         .scan_identifier_1char = TRUE,
75         .scan_identifier_NULL = FALSE,
76         .scan_symbols = TRUE,
77         .scan_binary = FALSE,
78         .scan_octal = FALSE,
79         .scan_float = TRUE,
80         .scan_hex = FALSE,
81         .scan_hex_dollar = FALSE,
82         .scan_string_sq = FALSE,
83         .scan_string_dq = FALSE,
84         .numbers_2_int = TRUE,
85         .int_2_float = FALSE,
86         .identifier_2_string = FALSE,
87         .char_2_token = TRUE,
88         .symbol_2_token = FALSE,
89         .scope_0_fallback = FALSE,
90         .store_int64 = FALSE
91     };
92
93     p = g_slice_new0(ObActionParser);
94     p->ref = 1;
95     p->scan = g_scanner_new(&config);
96     p->scan->input_name = source;
97     p->scan->msg_handler = display_error;
98     p->scan->user_data = p;
99     return p;
100 }
101
102 void action_parser_ref(ObActionParser *p)
103 {
104     ++p->ref;
105 }
106
107 void action_parser_unref(ObActionParser *p)
108 {
109     if (p && --p->ref < 1) {
110         g_scanner_destroy(p->scan);
111         g_slice_free(ObActionParser, p);
112     }
113 }
114
115 ObActionList* action_parser_read_string(ObActionParser *p, const gchar *text)
116 {
117     gboolean e;
118
119     g_scanner_input_text(p->scan, text, strlen(text));
120     if (!p->scan->input_name)
121         p->scan->input_name = "(console)";
122
123     e = FALSE;
124     return parse_list(p, G_TOKEN_EOF, &e);
125 }
126
127 ObActionList* actions_parser_read_file(ObActionParser *p,
128                                        const gchar *file,
129                                        GError **error)
130 {
131     GIOChannel *ch;
132     gboolean e;
133
134     ch = g_io_channel_new_file(file, "r", error);
135     if (!ch) return NULL;
136
137     g_scanner_input_file(p->scan, g_io_channel_unix_get_fd(ch));
138     p->scan->input_name = file;
139
140     e = FALSE;
141     return parse_list(p, G_TOKEN_EOF, &e);
142 }
143
144 void action_parser_set_on_error(ObActionParser *p, ObActionParserErrorFunc f,
145                                 gpointer data)
146 {
147     p->on_error = f;
148     p->on_error_user_data = data;
149 }
150
151 const ObActionParserError* action_parser_get_last_error(void)
152 {
153     return last_error.message ? &last_error : NULL;
154 }
155
156 void action_parser_reset_last_error(void)
157 {
158     g_free(last_error.source);
159     g_free(last_error.message);
160     last_error.source = last_error.message = NULL;
161 }
162
163 static void display_error(GScanner *scanner, gchar *message, gboolean error)
164 {
165     ObActionParser *p = (ObActionParser*) scanner->user_data;
166     guint line = scanner->line;
167     if (!p->on_error || p->on_error(&line, message, p->on_error_user_data)) {
168         action_parser_reset_last_error();
169
170         last_error.source = g_strdup(scanner->input_name);
171         last_error.line = line;
172         last_error.message = g_strdup(message);
173         last_error.is_error = error;
174         g_message("%s:%u: %s: %s",
175                   p->scan->input_name,
176                   last_error.line,
177                   (last_error.is_error ? "error" : "warning"),
178                   last_error.message);
179     }
180 }
181
182 gpointer parse_error(ObActionParser *p, GTokenType exp, const gchar *message,
183                      gboolean *e)
184 {
185     g_scanner_unexp_token(p->scan, exp, NULL, NULL, NULL, message, TRUE);
186     *e = TRUE;
187     return NULL;
188 }
189
190 /*************  Parser functions.  The list is the entry point.  ************
191 BNF for the language:
192
193 TEST       := KEY=VALUE | KEY
194 ACTION     := [FILTER] ACTION ELSE END | ACTIONNAME ACTIONOPTS | {ACTIONLIST}
195 ELSE       := nil | \| ACTION
196 END        := \n | ; | EOF
197 ACTIONLIST := ACTION ACTIONLIST | ACTION
198 FILTER     := FILTERORS
199 FILTERORS  := FILTERANDS \| FILTERORS | FILTERANDS
200 FILTERANDS := TEST, FILTERANDS | TEST
201 ACTIONOPTS := ACTIONOPT ACTIONOPTS | ACTIONOPT
202 ACTIONOPT  := ATTRIBUTE:WORD | ATTRIBUTE:STRING | ATTRIBUTE:{ACTIONLIST}
203 WORD   := string of text without any spaces
204 STRING := "TEXT" | (TEXT) |
205   where TEXT is a string, any occurance of the closing quote character must
206   be escaped by an backslash.
207   \\ \( \) and \" are all valid escaped characters.
208 **************                                                   ************/
209
210 ObActionList* parse_list(ObActionParser *p, GTokenType end, gboolean *e)
211 {
212     ObActionList *first, *last;
213     GTokenType t;
214
215     first = last = NULL;
216
217     t = g_scanner_peek_next_token(p->scan);
218     while (t != end && t != G_TOKEN_EOF) {
219         if (t == '\n')
220             g_scanner_get_next_token(p->scan); /* skip empty lines */
221         else if (t == ';') {
222             g_scanner_get_next_token(p->scan); /* separator */
223         }
224         else if (t == G_TOKEN_IDENTIFIER || t == '[' || t == '{') {
225             ObActionList *next;
226
227             /* parse the next action and stick it on the end of the list */
228             next = parse_action(p, e);
229             if (last) last->next = next;
230             if (!first) first = next;
231             last = next;
232         }
233         else {
234             g_scanner_get_next_token(p->scan);
235             parse_error(p, (end ? end : G_TOKEN_NONE),
236                         _("Expected an action or end of action list"), e);
237         }
238
239         if (*e) break; /* don't parse any more after an error */
240
241         t = g_scanner_peek_next_token(p->scan);
242     }
243
244     /* eat the ending character */
245     g_scanner_get_next_token(p->scan);
246
247     return first;
248 }
249
250 ObActionList* parse_action(ObActionParser *p, gboolean *e)
251 {
252     GTokenType t;
253     ObActionList *al;
254     gchar *name, *error_msg;
255     GHashTable *config;
256
257     t = g_scanner_get_next_token(p->scan);
258
259     if (t == '[') return parse_filter(p, e);
260     if (t == '{') return parse_list(p, '}', e);
261
262     /* check for a name */
263     if (t != G_TOKEN_IDENTIFIER)
264         return parse_error(p, G_TOKEN_NONE, _("Expected an action name"), e);
265
266     /* read the action's name */
267     name = g_strdup(p->scan->value.v_string);
268     config = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
269                                    (GDestroyNotify)config_value_unref);
270
271     /* read the action's options key:value pairs */
272     t = g_scanner_peek_next_token(p->scan);
273     while (t == G_TOKEN_IDENTIFIER || t == '\\') {
274         if (t == '\\') {
275             g_scanner_get_next_token(p->scan); /* eat it */
276             t = g_scanner_get_next_token(p->scan); /* check for '\n' */
277             if (t != '\n')
278                 parse_error(p, G_TOKEN_NONE, _("Expected newline"), e);
279         }
280         else {
281             gchar *key;
282             ObConfigValue *value;
283
284             g_scanner_get_next_token(p->scan); /* eat the key */
285             t = g_scanner_peek_next_token(p->scan); /* check for ':' */
286             if (t != ':') {
287                 g_scanner_get_next_token(p->scan);
288                 parse_error(p, ':', NULL, e);
289                 break; /* don't read any more options */
290             }
291
292             /* save the key */
293             key = g_strdup(p->scan->value.v_string);
294             g_scanner_get_next_token(p->scan); /* eat the ':' */
295
296             /* read the value */
297             value = parse_value(p, TRUE, e);
298
299             /* check if we read a value (regardless of errors), and save
300                the key:value pair if we did. */
301             if (value)
302                 g_hash_table_replace(config, key, value);
303             else
304                 g_free(key); /* didn't read any value */
305         }
306
307         if (*e) break; /* don't parse any more if there was an error */
308
309         t = g_scanner_peek_next_token(p->scan);
310     }
311
312     error_msg = NULL;
313
314     al = g_slice_new(ObActionList);
315     al->ref = 1;
316     al->isfilterset = FALSE;
317     al->u.action = action_new(name, config, &error_msg);
318     al->next = NULL;
319     g_free(name);
320     g_hash_table_unref(config);
321
322     if (error_msg) {
323         display_error(p->scan, error_msg, TRUE);
324         *e = TRUE;
325         g_free(error_msg);
326     }
327
328     return al;
329 }
330
331 ObActionList* parse_filter(ObActionParser *p, gboolean *e)
332 {
333     GTokenType t;
334     ObActionList *al, *thendo, *elsedo;
335     ObActionListTest *test;
336
337     /* read the filter tests */
338     test = parse_filter_test(p, e);
339     if (*e) {
340         action_list_test_destroy(test);
341         return NULL;
342     }
343
344     /* read the action for the filter */
345     thendo = parse_action(p, e);
346     elsedo = NULL;
347
348     if (!*e) {
349         /* check for else case */
350         t = g_scanner_peek_next_token(p->scan);
351         if (t == '|') {
352             g_scanner_get_next_token(p->scan); /* eat it */
353
354             /* read the else action for the filter */
355             elsedo = parse_action(p, e);
356         }
357     }
358
359     al = g_slice_new(ObActionList);
360     al->ref = 1;
361     al->isfilterset = TRUE;
362     al->u.f.test = test;
363     al->u.f.thendo = thendo;
364     al->u.f.elsedo = elsedo;
365     al->next = NULL;
366     return al;
367 }
368
369 ObActionListTest* parse_filter_test(ObActionParser *p, gboolean *e)
370 {
371     GTokenType t;
372     gchar *key;
373     ObConfigValue *value;
374     gboolean and;
375     ObActionFilter *filter;
376     ObActionListTest *next;
377
378     t = g_scanner_get_next_token(p->scan);
379     if (t == ']') /* empty */
380         return NULL;
381
382     else if (t != G_TOKEN_IDENTIFIER)
383         return parse_error(p, G_TOKEN_NONE,
384                            _("Expected a filter test lvalue"), e);
385
386     /* got a key */
387     key = g_strdup(p->scan->value.v_string);
388     value = NULL;
389     and = FALSE;
390     next = NULL;
391
392     /* check if it has a value also */
393     t = g_scanner_peek_next_token(p->scan);
394     if (t == '=') {
395         g_scanner_get_next_token(p->scan); /* eat the = */
396         value = parse_value(p, FALSE, e);
397     }
398
399     /* don't allow any errors (but value can be null if not present) */
400     if (*e) {
401         g_free(key);
402         config_value_unref(value);
403         return NULL;
404     }
405
406     filter = action_filter_new(key, value);
407     if (!filter) {
408         gchar *m;
409         m = g_strdup_printf(_("Unable to create filter: %s"), key);
410         parse_error(p, G_TOKEN_NONE, m, e);
411         g_free(m);
412     }
413
414     g_free(key);
415     config_value_unref(value);
416     if (!filter)
417         return NULL;
418
419     /* check if there is another test and how we're connected */
420     t = g_scanner_get_next_token(p->scan);
421     if (t == ',') { /* and */
422         and = TRUE;
423         next = parse_filter_test(p, e);
424     }
425     else if (t == '|') { /* or */
426         and = FALSE;
427         next = parse_filter_test(p, e);
428     }
429     else if (t == ']') /* end of the filter */
430         ;
431     else
432         parse_error(p, ']', NULL, e);
433
434     /* don't allow any errors */
435     if (*e) {
436         action_filter_unref(filter);
437         action_list_test_destroy(next);
438         return NULL;
439     }
440     else {
441         ObActionListTest *test;
442
443         test = g_slice_new(ObActionListTest);
444         test->filter = filter;
445         test->and = and;
446         test->next = next;
447         return test;
448     }
449 }
450
451 ObConfigValue* parse_value(ObActionParser *p,
452                            gboolean allow_actions,
453                            gboolean *e)
454 {
455     GTokenType t;
456     ObConfigValue *v;
457
458     v = NULL;
459     t = g_scanner_get_next_token(p->scan);
460     if (t == G_TOKEN_IDENTIFIER) {
461         v = config_value_new_string(p->scan->value.v_string);
462     }
463     else if (t == '"')
464         v = config_value_new_string(parse_string(p, '"', e));
465     else if (t == '(')
466         v = config_value_new_string(parse_string(p, ')', e));
467     else if (t == '{' && allow_actions) {
468         ObActionList *l = parse_list(p, '}', e);
469         if (l) v = config_value_new_action_list(l);
470     }
471     else
472         parse_error(p, G_TOKEN_NONE, _("Expected an option value"), e);
473     return v;
474 }
475
476
477 gchar* parse_string(ObActionParser *p, guchar end, gboolean *e)
478 {
479     GString *buf;
480     GTokenType t;
481     const gchar *error_message = NULL;
482
483     /* inside a string we want everything to be parsed as text (identifiers) */
484     p->scan->config->cset_skip_characters = "";
485     p->scan->config->cset_identifier_first = IDENTIFIER_NTH " ";
486     p->scan->config->cset_identifier_nth = IDENTIFIER_NTH " ";
487
488     buf = g_string_new("");
489
490     t = g_scanner_get_next_token(p->scan);
491     while (t != end) {
492         if (t == G_TOKEN_IDENTIFIER)
493             g_string_append(buf, p->scan->value.v_string);
494         else if (t == G_TOKEN_EOF) {
495             error_message = _("Missing end of quoted string");
496             goto parse_string_error;
497         }
498         else if (t == '\\') { /* escape sequence */
499             t = g_scanner_get_next_token(p->scan);
500             if (!strchr(ESCAPE_SEQS, t)) {
501                 error_message = _("Unknown escape sequence");
502                 goto parse_string_error;
503             }
504             g_string_append_c(buf, t);
505         }
506         else /* other single character */
507             g_string_append_c(buf, t);
508
509         t = g_scanner_get_next_token(p->scan);
510     }
511
512     /* reset to their default values */
513     p->scan->config->cset_skip_characters = SKIP;
514     p->scan->config->cset_identifier_first = IDENTIFIER_FIRST;
515     p->scan->config->cset_identifier_nth = IDENTIFIER_NTH;
516
517     {
518         gchar *v = buf->str;
519         g_string_free(buf, FALSE);
520         return v;
521     }
522
523 parse_string_error:
524     g_scanner_unexp_token(p->scan, G_TOKEN_NONE, NULL, NULL, NULL,
525                           error_message, TRUE);
526     g_string_free(buf, TRUE);
527     *e = TRUE;
528     return NULL;
529 }