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