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