make an event queue for X events. the queue's min size is 16 XEvents (~3k)
[dana/openbox.git] / obt / ddparse.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    obt/ddparse.c for the Openbox window manager
4    Copyright (c) 2009        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 "obt/ddparse.h"
20 #include "obt/link.h"
21 #ifdef HAVE_STRING_H
22 #include <string.h>
23 #endif
24 #ifdef HAVE_STDIO_H
25 #include <stdio.h>
26 #endif
27
28 typedef struct _ObtDDParse ObtDDParse;
29
30 /* Parses the value and adds it to the group's key_hash, with the given
31    key
32    Return TRUE if it is added to the hash table, and FALSE if not.
33 */
34 typedef gboolean (*ObtDDParseValueFunc)(gchar *key, const gchar *val,
35                                         ObtDDParse *parse, gboolean *error);
36
37
38 struct _ObtDDParse {
39     gchar *filename;
40     gulong lineno;
41     ObtDDParseGroup *group;
42     /* the key is a group name, the value is a ObtDDParseGroup */
43     GHashTable *group_hash;
44 };
45
46 struct _ObtDDParseGroup {
47     gchar *name;
48     gboolean seen;
49     ObtDDParseValueFunc value_func;
50     /* the key is a string (a key inside the group in the .desktop).
51        the value is an ObtDDParseValue */
52     GHashTable *key_hash;
53 };
54
55 /* Displays a warning message including the file name and line number, and
56    sets the boolean @error to true if it points to a non-NULL address.
57 */
58 static void parse_error(const gchar *m, const ObtDDParse *const parse,
59                         gboolean *error)
60 {
61     if (!parse->filename)
62         g_warning("%s at line %lu of input", m, parse->lineno);
63     else
64         g_warning("%s at line %lu of file %s",
65                   m, parse->lineno, parse->filename);
66     if (error) *error = TRUE;
67 }
68
69 static void parse_value_free(ObtDDParseValue *v)
70 {
71     switch (v->type) {
72     case OBT_DDPARSE_STRING:
73     case OBT_DDPARSE_LOCALESTRING:
74         g_free(v->value.string); break;
75     case OBT_DDPARSE_STRINGS:
76     case OBT_DDPARSE_LOCALESTRINGS:
77         g_free(v->value.strings.s);
78         v->value.strings.n = 0;
79         break;
80     case OBT_DDPARSE_BOOLEAN:
81     case OBT_DDPARSE_NUMERIC:
82     case OBT_DDPARSE_ENUM_APPLICATION:
83         break;
84     default:
85         g_assert_not_reached();
86     }
87     g_slice_free(ObtDDParseValue, v);
88 }
89
90 static ObtDDParseGroup* parse_group_new(gchar *name, ObtDDParseValueFunc f)
91 {
92     ObtDDParseGroup *g = g_slice_new(ObtDDParseGroup);
93     g->name = name;
94     g->value_func = f;
95     g->seen = FALSE;
96     g->key_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
97                                         g_free,
98                                         (GDestroyNotify)parse_value_free);
99     return g;
100 }
101
102 static void parse_group_free(ObtDDParseGroup *g)
103 {
104     g_free(g->name);
105     g_hash_table_destroy(g->key_hash);
106     g_slice_free(ObtDDParseGroup, g);
107 }
108
109 /*! Reads an input string, strips out invalid stuff, and parses
110     backslash-stuff.
111     If @nstrings is not NULL, then it splits the output string at ';'
112     characters.  They are all returned in the same string with null zeros
113     between them, @nstrings is set to the number of such strings.
114  */
115 static gchar* parse_value_string(const gchar *in,
116                                  gboolean locale,
117                                  gulong *nstrings,
118                                  const ObtDDParse *const parse,
119                                  gboolean *error)
120 {
121     const gint bytes = strlen(in);
122     gboolean backslash;
123     gchar *out, *o;
124     const gchar *end, *i;
125
126     g_return_val_if_fail(in != NULL, NULL);
127
128     if (!locale) {
129         end = in + bytes;
130         for (i = in; i < end; ++i) {
131             if ((guchar)*i >= 127 || (guchar)*i < 32) {
132                 /* non-control character ascii */
133                 end = i;
134                 parse_error("Invalid bytes in string", parse, error);
135                 break;
136             }
137         }
138     }
139     else if (!g_utf8_validate(in, bytes, &end))
140         parse_error("Invalid bytes in localestring", parse, error);
141
142     if (nstrings) *nstrings = 1;
143
144     out = g_new(char, bytes + 1);
145     i = in; o = out;
146     backslash = FALSE;
147     while (i < end) {
148         const gchar *next = locale ? g_utf8_find_next_char(i, end) : i+1;
149         if (backslash) {
150             switch(*i) {
151             case 's': *o++ = ' '; break;
152             case 'n': *o++ = '\n'; break;
153             case 't': *o++ = '\t'; break;
154             case 'r': *o++ = '\r'; break;
155             case ';': *o++ = ';'; break;
156             case '\\': *o++ = '\\'; break;
157             default:
158                 parse_error((locale ?
159                              "Invalid escape sequence in localestring" :
160                              "Invalid escape sequence in string"),
161                             parse, error);
162             }
163             backslash = FALSE;
164         }
165         else if (*i == '\\')
166             backslash = TRUE;
167         else if (*i == ';' && nstrings) {
168             ++nstrings;
169             *o = '\0';
170         }
171         else if ((guchar)*i == 127 || (guchar)*i < 32) {
172             /* avoid ascii control characters */
173             parse_error("Found control character in string", parse, error);
174             break;
175         }
176         else {
177             memcpy(o, i, next-i);
178             o += next-i;
179         }
180         i = next;
181     }
182     *o = '\0';
183     return o;
184 }
185
186 static gboolean parse_value_boolean(const gchar *in,
187                                     const ObtDDParse *const parse,
188                                     gboolean *error)
189 {
190     if (strcmp(in, "true") == 0)
191         return TRUE;
192     else if (strcmp(in, "false") != 0)
193         parse_error("Invalid boolean value", parse, error);
194     return FALSE;
195 }
196
197 static gfloat parse_value_numeric(const gchar *in,
198                                   const ObtDDParse *const parse,
199                                   gboolean *error)
200 {
201     gfloat out = 0;
202     if (sscanf(in, "%f", &out) == 0)
203         parse_error("Invalid numeric value", parse, error);
204     return out;
205 }
206
207 static gboolean parse_file_line(FILE *f, gchar **buf,
208                                 gulong *size, gulong *read,
209                                 ObtDDParse *parse, gboolean *error)
210 {
211     const gulong BUFMUL = 80;
212     size_t ret;
213     gulong i, null;
214
215     if (*size == 0) {
216         g_assert(*read == 0);
217         *size = BUFMUL;
218         *buf = g_new(char, *size);
219     }
220
221     /* remove everything up to a null zero already in the buffer and shift
222        the rest to the front */
223     null = *size;
224     for (i = 0; i < *read; ++i) {
225         if (null < *size)
226             (*buf)[i-null-1] = (*buf)[i];
227         else if ((*buf)[i] == '\0')
228             null = i;
229     }
230     if (null < *size)
231         *read -= null + 1;
232
233     /* is there already a newline in the buffer? */
234     for (i = 0; i < *read; ++i)
235         if ((*buf)[i] == '\n') {
236             /* turn it into a null zero and done */
237             (*buf)[i] = '\0';
238             return TRUE;
239         }
240
241     /* we need to read some more to find a newline */
242     while (TRUE) {
243         gulong eol;
244         gchar *newread;
245
246         newread = *buf + *read;
247         ret = fread(newread, sizeof(char), *size-*read, f);
248         if (ret < *size - *read && !feof(f)) {
249             parse_error("Error reading", parse, error);
250             return FALSE;
251         }
252         *read += ret;
253
254         /* strip out null zeros in the input and look for an endofline */
255         null = 0;
256         eol = *size;
257         for (i = newread-*buf; i < *read; ++i) {
258             if (null > 0)
259                 (*buf)[i] = (*buf)[i+null];
260             if ((*buf)[i] == '\0') {
261                 ++null;
262                 --(*read);
263                 --i; /* try again */
264             }
265             else if ((*buf)[i] == '\n' && eol == *size) {
266                 eol = i;
267                 /* turn it into a null zero */
268                 (*buf)[i] = '\0';
269             }
270         }
271
272         if (eol != *size)
273             /* found an endofline, done */
274             break;
275         else if (feof(f) && *read < *size) {
276             /* found the endoffile, done (if there is space) */
277             if (*read > 0) {
278                 /* stick a null zero on if there is test on the last line */
279                 (*buf)[(*read)++] = '\0';
280             }
281             break;
282         }
283         else {
284             /* read more */
285             size += BUFMUL;
286             *buf = g_renew(char, *buf, *size);
287         }
288     }
289     return *read > 0;
290 }
291
292 static void parse_group(const gchar *buf, gulong len,
293                         ObtDDParse *parse, gboolean *error)
294 {
295     ObtDDParseGroup *g;
296     gchar *group;
297     gulong i;
298
299     /* get the group name */
300     group = g_strndup(buf+1, len-2);
301     for (i = 0; i < len-2; ++i)
302         if ((guchar)group[i] < 32 || (guchar)group[i] >= 127) {
303             /* valid ASCII only */
304             parse_error("Invalid character found", parse, NULL);
305             group[i] = '\0'; /* stopping before this character */
306             break;
307         }
308
309     /* make sure it's a new group */
310     g = g_hash_table_lookup(parse->group_hash, group);
311     if (g && g->seen) {
312         parse_error("Duplicate group found", parse, error);
313         g_free(group);
314         return;
315     }
316     /* if it's the first group, make sure it's named Desktop Entry */
317     else if (!parse->group && strcmp(group, "Desktop Entry") != 0)
318     {
319         parse_error("Incorrect group found, "
320                     "expected [Desktop Entry]",
321                     parse, error);
322         g_free(group);
323         return;
324     }
325     else {
326         if (!g) {
327             g = parse_group_new(group, NULL);
328             g_hash_table_insert(parse->group_hash, g->name, g);
329         }
330         else
331             g_free(group);
332
333         g->seen = TRUE;
334         parse->group = g;
335         g_print("Found group %s\n", g->name);
336     }
337 }
338
339 static void parse_key_value(const gchar *buf, gulong len,
340                             ObtDDParse *parse, gboolean *error)
341 {
342     gulong i, keyend, valstart, eq;
343     char *key;
344
345     /* find the end of the key */
346     for (i = 0; i < len; ++i)
347         if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
348               ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z') ||
349               ((guchar)buf[i] >= '0' && (guchar)buf[i] <= '9') ||
350               ((guchar)buf[i] == '-'))) {
351             /* not part of the key */
352             keyend = i;
353             break;
354         }
355     if (keyend < 1) {
356         parse_error("Empty key", parse, error);
357         return;
358     }
359     /* find the = character */
360     for (i = keyend; i < len; ++i) {
361         if (buf[i] == '=') {
362             eq = i;
363             break;
364         }
365         else if (buf[i] != ' ') {
366             parse_error("Invalid character in key name", parse, error);
367             return ;
368         }
369     }
370     if (i == len) {
371         parse_error("Key without value found", parse, error);
372         return;
373     }
374     /* find the start of the value */
375     for (i = eq+1; i < len; ++i)
376         if (buf[i] != ' ') {
377             valstart = i;
378             break;
379         }
380     if (i == len) {
381         parse_error("Empty value found", parse, error);
382         return;
383     }
384
385     key = g_strndup(buf, keyend);
386     if (g_hash_table_lookup(parse->group->key_hash, key)) {
387         parse_error("Duplicate key found", parse, error);
388         g_free(key);
389         return;
390     }
391     g_print("Found key/value %s=%s.\n", key, buf+valstart);
392     if (parse->group->value_func)
393         if (!parse->group->value_func(key, buf+valstart, parse, error)) {
394             parse_error("Unknown key", parse, error);
395             g_free(key);
396         }
397 }
398
399 static gboolean parse_file(FILE *f, ObtDDParse *parse)
400 {
401     gchar *buf = NULL;
402     gulong bytes = 0, read = 0;
403     gboolean error = FALSE;
404
405     while (!error && parse_file_line(f, &buf, &bytes, &read, parse, &error)) {
406         gulong len = strlen(buf);
407         if (buf[0] == '#' || buf[0] == '\0')
408             ; /* ignore comment lines */
409         else if (buf[0] == '[' && buf[len-1] == ']')
410             parse_group(buf, len, parse, &error);
411         else if (!parse->group)
412             /* just ignore keys outside of groups */
413             parse_error("Key found before group", parse, NULL);
414         else
415             /* ignore errors in key-value pairs and continue */
416             parse_key_value(buf, len, parse, NULL);
417         ++parse->lineno;
418     }
419
420     if (buf) g_free(buf);
421     return !error;
422 }
423
424 static gboolean parse_desktop_entry_value(gchar *key, const gchar *val,
425                                           ObtDDParse *parse, gboolean *error)
426 {
427     ObtDDParseValue v, *pv;
428
429     switch (key[0]) {
430     case 'C':
431         switch (key[1]) {
432         case 'a': /* Categories */
433             if (strcmp(key+2, "tegories")) return FALSE;
434             v.type = OBT_DDPARSE_STRINGS; break;
435         case 'o': /* Comment */
436             if (strcmp(key+2, "mment")) return FALSE;
437             v.type = OBT_DDPARSE_LOCALESTRING; break;
438         default:
439             return FALSE;
440         }
441         break;
442     case 'E': /* Exec */
443         if (strcmp(key+1, "xec")) return FALSE;
444         v.type = OBT_DDPARSE_STRING; break;
445     case 'G': /* GenericName */
446         if (strcmp(key+1, "enericName")) return FALSE;
447         v.type = OBT_DDPARSE_LOCALESTRING; break;
448     case 'I': /* Icon */
449         if (strcmp(key+1, "con")) return FALSE;
450         v.type = OBT_DDPARSE_LOCALESTRING; break;
451     case 'H': /* Hidden */
452         if (strcmp(key+1, "idden")) return FALSE;
453         v.type = OBT_DDPARSE_BOOLEAN; break;
454     case 'M': /* MimeType */
455         if (strcmp(key+1, "imeType")) return FALSE;
456         v.type = OBT_DDPARSE_STRINGS; break;
457     case 'N':
458         switch (key[1]) {
459         case 'a': /* Name */
460             if (strcmp(key+2, "me")) return FALSE;
461             v.type = OBT_DDPARSE_LOCALESTRING; break;
462         case 'o':
463             switch (key[2]) {
464             case 'D': /* NoDisplay */
465                 if (strcmp(key+3, "isplay")) return FALSE;
466                 v.type = OBT_DDPARSE_BOOLEAN; break;
467             case 't': /* NotShowIn */
468                 if (strcmp(key+3, "ShowIn")) return FALSE;
469                 v.type = OBT_DDPARSE_STRINGS; break;
470             default:
471                 return FALSE;
472             }
473             break;
474         default:
475             return FALSE;
476         }
477         break;
478     case 'P': /* Path */
479         if (strcmp(key+1, "ath")) return FALSE;
480         v.type = OBT_DDPARSE_STRING; break;
481     case 'S': /* Path */
482         if (key[1] == 't' && key[2] == 'a' && key[3] == 'r' &&
483             key[4] == 't' && key[5] == 'u' && key[6] == 'p')
484             switch (key[7]) {
485             case 'N': /* StartupNotify */
486                 if (strcmp(key+8, "otify")) return FALSE;
487                 v.type = OBT_DDPARSE_BOOLEAN; break;
488             case 'W': /* StartupWMClass */
489                 if (strcmp(key+8, "MClass")) return FALSE;
490                 v.type = OBT_DDPARSE_STRING; break;
491             default:
492                 return FALSE;
493             }
494         else
495             return FALSE;
496         break;
497     case 'T':
498         switch (key[1]) {
499         case 'e': /* Terminal */
500             if (strcmp(key+2, "rminal")) return FALSE;
501             v.type = OBT_DDPARSE_BOOLEAN; break;
502         case 'r': /* TryExec */
503             if (strcmp(key+2, "yExec")) return FALSE;
504             v.type = OBT_DDPARSE_STRING; break;
505         case 'y': /* Type */
506             if (strcmp(key+2, "pe")) return FALSE;
507             v.type = OBT_DDPARSE_STRING; break;
508         default:
509             return FALSE;
510         }
511         break;
512     case 'U': /* URL */
513         if (strcmp(key+1, "RL")) return FALSE;
514         v.type = OBT_DDPARSE_STRING; break;
515     case 'V': /* MimeType */
516         if (strcmp(key+1, "ersion")) return FALSE;
517         v.type = OBT_DDPARSE_STRING; break;
518     default:
519         return FALSE;
520     }
521
522     /* parse the value */
523     switch (v.type) {
524     case OBT_DDPARSE_STRING:
525         v.value.string = parse_value_string(val, FALSE, NULL, parse, error);
526         g_assert(v.value.string);
527         break;
528     case OBT_DDPARSE_LOCALESTRING:
529         v.value.string = parse_value_string(val, TRUE, NULL, parse, error);
530         g_assert(v.value.string);
531         break;
532     case OBT_DDPARSE_STRINGS:
533         v.value.strings.s = parse_value_string(val, FALSE, &v.value.strings.n,
534                                                parse, error);
535         g_assert(v.value.strings.s);
536         g_assert(v.value.strings.n);
537         break;
538     case OBT_DDPARSE_LOCALESTRINGS:
539         v.value.strings.s = parse_value_string(val, TRUE, &v.value.strings.n,
540                                                parse, error);
541         g_assert(v.value.strings.s);
542         g_assert(v.value.strings.n);
543         break;
544     case OBT_DDPARSE_BOOLEAN:
545         v.value.boolean = parse_value_boolean(val, parse, error);
546         break;
547     case OBT_DDPARSE_NUMERIC:
548         v.value.numeric = parse_value_numeric(val, parse, error);
549         break;
550     case OBT_DDPARSE_ENUM_APPLICATION:
551         if (val[0] == 'A' && strcmp(val+1, "pplication") == 0)
552             v.value.enumerable = OBT_LINK_TYPE_APPLICATION;
553         else if (val[0] == 'L' && strcmp(val+1, "ink") == 0)
554             v.value.enumerable = OBT_LINK_TYPE_URL;
555         else if (val[0] == 'D' && strcmp(val+1, "irectory") == 0)
556             v.value.enumerable = OBT_LINK_TYPE_DIRECTORY;
557         else {
558             parse_error("Unknown Type", parse, error);
559             return FALSE;
560         }
561         break;
562     default:
563         g_assert_not_reached();
564     }
565
566     pv = g_slice_new(ObtDDParseValue);
567     *pv = v;
568     g_hash_table_insert(parse->group->key_hash, key, pv);
569     return TRUE;
570 }
571
572 GHashTable* obt_ddparse_file(const gchar *name, GSList *paths)
573 {
574     ObtDDParse parse;
575     ObtDDParseGroup *desktop_entry;
576     GSList *it;
577     FILE *f;
578     gboolean success;
579
580     parse.filename = NULL;
581     parse.lineno = 0;
582     parse.group = NULL;
583     parse.group_hash = g_hash_table_new_full(g_str_hash,
584                                              g_str_equal,
585                                              NULL,
586                                              (GDestroyNotify)parse_group_free);
587
588     /* set up the groups (there's only one right now) */
589     desktop_entry = parse_group_new(g_strdup("Desktop Entry"),
590                                     parse_desktop_entry_value);
591     g_hash_table_insert(parse.group_hash, desktop_entry->name, desktop_entry);
592
593     success = FALSE;
594     for (it = paths; it && !success; it = g_slist_next(it)) {
595         gchar *path = g_strdup_printf("%s/%s", (char*)it->data, name);
596         if ((f = fopen(path, "r"))) {
597             parse.filename = path;
598             parse.lineno = 1;
599             success = parse_file(f, &parse);
600             fclose(f);
601         }
602         g_free(path);
603     }
604     if (!success) {
605         g_hash_table_destroy(parse.group_hash);
606         return NULL;
607     }
608     else
609         return parse.group_hash;
610 }
611
612 GHashTable* obt_ddparse_group_keys(ObtDDParseGroup *g)
613 {
614     return g->key_hash;
615 }