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