]> icculus.org git repositories - dana/openbox.git/blob - obt/ddparse.c
wip: loading theme stuff
[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                                         ObtDDParseLangMatch match,
36                                         ObtDDParse *parse, gboolean *error);
37
38
39 enum {
40     DE_TYPE             = 1 << 0,
41     DE_TYPE_APPLICATION = 1 << 1,
42     DE_TYPE_LINK        = 1 << 2,
43     DE_NAME             = 1 << 3,
44     DE_EXEC             = 1 << 4,
45     DE_URL              = 1 << 5
46 };
47
48 struct _ObtDDParse {
49     const gchar *filename;
50     const gchar *language;
51     const gchar *country;
52     const gchar *modifier;
53     gulong lineno;
54     gulong flags;
55     ObtDDParseGroup *group;
56     /* the key is a group name, the value is a ObtDDParseGroup */
57     GHashTable *group_hash;
58 };
59
60 struct _ObtDDParseGroup {
61     gchar *name;
62     gboolean seen;
63     ObtDDParseValueFunc value_func;
64     /* the key is a string (a key inside the group in the .desktop).
65        the value is an ObtDDParseValue */
66     GHashTable *key_hash;
67 };
68
69 /* Displays a warning message including the file name and line number, and
70    sets the boolean @error to true if it points to a non-NULL address.
71 */
72 static void parse_error(const gchar *m, const ObtDDParse *const parse,
73                         gboolean *error)
74 {
75     if (!parse->filename)
76         g_debug("%s at line %lu of input", m, parse->lineno);
77     else
78         g_debug("%s at line %lu of file %s",
79                   m, parse->lineno, parse->filename);
80     if (error) *error = TRUE;
81 }
82
83 static void parse_value_free(ObtDDParseValue *v)
84 {
85     switch (v->type) {
86     case OBT_DDPARSE_EXEC:
87     case OBT_DDPARSE_STRING:
88     case OBT_DDPARSE_LOCALESTRING:
89         g_free(v->value.string); break;
90     case OBT_DDPARSE_STRINGS:
91     case OBT_DDPARSE_LOCALESTRINGS:
92         g_strfreev(v->value.strings.a);
93         v->value.strings.n = 0;
94         break;
95     case OBT_DDPARSE_BOOLEAN:
96     case OBT_DDPARSE_NUMERIC:
97     case OBT_DDPARSE_ENUM_TYPE:
98     case OBT_DDPARSE_ENVIRONMENTS:
99         break;
100     default:
101         g_assert_not_reached();
102     }
103     g_slice_free(ObtDDParseValue, v);
104 }
105
106 static ObtDDParseGroup* parse_group_new(gchar *name, ObtDDParseValueFunc f)
107 {
108     ObtDDParseGroup *g = g_slice_new(ObtDDParseGroup);
109     g->name = name;
110     g->value_func = f;
111     g->seen = FALSE;
112     g->key_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
113                                         g_free,
114                                         (GDestroyNotify)parse_value_free);
115     return g;
116 }
117
118 static void parse_group_free(ObtDDParseGroup *g)
119 {
120     g_free(g->name);
121     g_hash_table_destroy(g->key_hash);
122     g_slice_free(ObtDDParseGroup, g);
123 }
124
125 /*! Reads an input string, strips out invalid stuff, and parses
126     backslash-stuff.
127  */
128 static gchar* parse_value_string(const gchar *in,
129                                  gboolean locale,
130                                  gboolean semicolonterminate,
131                                  gulong *len,
132                                  const ObtDDParse *const parse,
133                                  gboolean *error)
134 {
135     gint bytes;
136     gboolean backslash;
137     gchar *out, *o;
138     const gchar *end, *i;
139
140     /* find the end/size of the string */
141     backslash = FALSE;
142     for (end = in; *end; ++end) {
143         if (semicolonterminate) {
144             if (backslash) backslash = FALSE;
145             else if (*end == '\\') backslash = TRUE;
146             else if (*end == ';') break;
147         }
148     }
149     bytes = end - in;
150
151     g_return_val_if_fail(in != NULL, NULL);
152
153     if (locale && !g_utf8_validate(in, bytes, &end)) {
154         parse_error("Invalid bytes in localestring", parse, error);
155         bytes = end - in;
156     }
157
158     out = g_new(char, bytes + 1);
159     if (len) *len = 0;
160     i = in; o = out;
161     backslash = FALSE;
162     while (i < end) {
163         const gchar *next;
164
165         /* find the next character in the string */
166         if (!locale) next = i+1;
167         else if (!(next = g_utf8_find_next_char(i, end))) next = end;
168
169         if (backslash) {
170             switch(*i) {
171             case 's': *o++ = ' '; break;
172             case 'n': *o++ = '\n'; break;
173             case 't': *o++ = '\t'; break;
174             case 'r': *o++ = '\r'; break;
175             case ';': *o++ = ';'; break;
176             case '\\': *o++ = '\\'; break;
177             default:
178                 parse_error((locale ?
179                              "Invalid escape sequence in localestring" :
180                              "Invalid escape sequence in string"),
181                             parse, error);
182             }
183             backslash = FALSE;
184         }
185         else if (*i == '\\')
186             backslash = TRUE;
187         else if ((!locale && (guchar)*i >= 127) || (guchar)*i < 32) {
188             /* avoid ascii control characters */
189             parse_error("Found control character in string", parse, error);
190             break;
191         }
192         else {
193             const gulong s = next-i;
194             memcpy(o, i, s);
195             o += s;
196             if (len) *len += s;
197         }
198         i = next;
199     }
200     *o = '\0';
201     return out;
202 }
203
204
205 /*! Reads a list of input strings, strips out invalid stuff, and parses
206     backslash-stuff.
207  */
208 static gchar** parse_value_strings(const gchar *in,
209                                    gboolean locale,
210                                    gulong *nstrings,
211                                    const ObtDDParse *const parse,
212                                    gboolean *error)
213 {
214     gchar **out;
215     const gchar *i;
216
217     out = g_new(gchar*, 1);
218     out[0] = NULL;
219     *nstrings = 0;
220
221     i = in;
222     while (TRUE) {
223         gchar *a;
224         gulong len;
225
226         a = parse_value_string(i, locale, TRUE, &len, parse, error);
227         i += len;
228
229         if (len) {
230             (*nstrings)++;
231             out = g_renew(gchar*, out, *nstrings+1);
232             out[*nstrings-1] = a;
233             out[*nstrings] = NULL;
234         }
235
236         if (!*i) break; /* no more strings */
237         ++i;
238     }
239     return out;
240 }
241
242 static guint parse_value_environments(const gchar *in,
243                                       const ObtDDParse *const parse,
244                                       gboolean *error)
245 {
246     const gchar *s;
247     guint mask = 0;
248
249     s = in;
250     while (*s) {
251         switch (*(s++)) {
252         case 'G':
253             if (strncmp(s, "NOME", 4) == 0 ||
254                 strncmp(s, "NOME;", 5 == 0))
255             {
256                 mask |= OBT_LINK_ENV_GNOME;
257                 s += 4;
258             }
259             break;
260         case 'K':
261             if (strncmp(s, "DE", 2) == 0 ||
262                 strncmp(s, "DE;", 3) == 0)
263             {
264                 mask |= OBT_LINK_ENV_KDE;
265                 s += 2;
266             }
267             break;
268         case 'L':
269             if (strncmp(s, "XDE", 3) == 0 ||
270                 strncmp(s, "XDE;", 4) == 0)
271             {
272                 mask |= OBT_LINK_ENV_LXDE;
273                 s += 3;
274             }
275             break;
276         case 'R':
277             if (strncmp(s, "OX", 2) == 0 ||
278                 strncmp(s, "OX;", 3) == 0)
279             {
280                 mask |= OBT_LINK_ENV_ROX;
281                 s += 2;
282             }
283             break;
284         case 'X':
285             if (strncmp(s, "FCE", 3) == 0 ||
286                 strncmp(s, "FCE;", 4) == 0)
287             {
288                 mask |= OBT_LINK_ENV_XFCE;
289                 s += 3;
290             }
291             break;
292         case 'O':
293             switch (*(s++)) {
294             case 'l':
295                 if (strncmp(s, "d", 1) == 0 ||
296                     strncmp(s, "d;", 2) == 0)
297                 {
298                     mask |= OBT_LINK_ENV_OLD;
299                     s += 1;
300                 }
301                 break;
302             case 'P':
303                 if (strncmp(s, "ENBOX", 5) == 0 ||
304                     strncmp(s, "ENBOX;", 6) == 0)
305                 {
306                     mask |= OBT_LINK_ENV_OPENBOX;
307                     s += 5;
308                 }
309                 break;
310             }
311         }
312         /* find the next string, or the end of the sequence */
313         while (*s && *s != ';') ++s;
314     }
315     return mask;
316 }
317
318 static gboolean parse_value_boolean(const gchar *in,
319                                     const ObtDDParse *const parse,
320                                     gboolean *error)
321 {
322     if (strcmp(in, "true") == 0)
323         return TRUE;
324     else if (strcmp(in, "false") != 0)
325         parse_error("Invalid boolean value", parse, error);
326     return FALSE;
327 }
328
329 static gfloat parse_value_numeric(const gchar *in,
330                                   const ObtDDParse *const parse,
331                                   gboolean *error)
332 {
333     gfloat out = 0;
334     if (sscanf(in, "%f", &out) == 0)
335         parse_error("Invalid numeric value", parse, error);
336     return out;
337 }
338
339 static gboolean parse_file_line(FILE *f, gchar **buf,
340                                 gulong *size, gulong *read,
341                                 ObtDDParse *parse, gboolean *error)
342 {
343     const gulong BUFMUL = 80;
344     size_t ret;
345     gulong i, null;
346
347     if (*size == 0) {
348         g_assert(*read == 0);
349         *size = BUFMUL;
350         *buf = g_new(char, *size);
351     }
352
353     /* remove everything up to a null zero already in the buffer and shift
354        the rest to the front */
355     null = *size;
356     for (i = 0; i < *read; ++i) {
357         if (null < *size)
358             (*buf)[i-null-1] = (*buf)[i];
359         else if ((*buf)[i] == '\0')
360             null = i;
361     }
362     if (null < *size)
363         *read -= null + 1;
364
365     /* is there already a newline in the buffer? */
366     for (i = 0; i < *read; ++i)
367         if ((*buf)[i] == '\n') {
368             /* turn it into a null zero and done */
369             (*buf)[i] = '\0';
370             return TRUE;
371         }
372
373     /* we need to read some more to find a newline */
374     while (TRUE) {
375         gulong eol;
376         gchar *newread;
377
378         newread = *buf + *read;
379         ret = fread(newread, sizeof(char), *size-*read, f);
380         if (ret < *size - *read && !feof(f)) {
381             parse_error("Error reading", parse, error);
382             return FALSE;
383         }
384         *read += ret;
385
386         /* strip out null zeros in the input and look for an endofline */
387         null = 0;
388         eol = *size;
389         for (i = newread-*buf; i < *read; ++i) {
390             if (null > 0)
391                 (*buf)[i] = (*buf)[i+null];
392             if ((*buf)[i] == '\0') {
393                 ++null;
394                 --(*read);
395                 --i; /* try again */
396             }
397             else if ((*buf)[i] == '\n' && eol == *size) {
398                 eol = i;
399                 /* turn it into a null zero */
400                 (*buf)[i] = '\0';
401             }
402         }
403
404         if (eol != *size)
405             /* found an endofline, done */
406             break;
407         else if (feof(f) && *read < *size) {
408             /* found the endoffile, done (if there is space) */
409             if (*read > 0) {
410                 /* stick a null zero on if there is test on the last line */
411                 (*buf)[(*read)++] = '\0';
412             }
413             break;
414         }
415         else {
416             /* read more */
417             *size += BUFMUL;
418             *buf = g_renew(char, *buf, *size);
419         }
420     }
421     return *read > 0;
422 }
423
424 static void parse_group(const gchar *buf, gulong len,
425                         ObtDDParse *parse, gboolean *error)
426 {
427     ObtDDParseGroup *g;
428     gchar *group;
429     gulong i;
430
431     /* get the group name */
432     group = g_strndup(buf+1, len-2);
433     for (i = 0; i < len-2; ++i)
434         if ((guchar)group[i] < 32 || (guchar)group[i] >= 127) {
435             /* valid ASCII only */
436             parse_error("Invalid character found", parse, NULL);
437             group[i] = '\0'; /* stopping before this character */
438             break;
439         }
440
441     /* make sure it's a new group */
442     g = g_hash_table_lookup(parse->group_hash, group);
443     if (g && g->seen) {
444         parse_error("Duplicate group found", parse, error);
445         g_free(group);
446         return;
447     }
448     /* if it's the first group, make sure it's named Desktop Entry */
449     else if (!parse->group && strcmp(group, "Desktop Entry") != 0)
450     {
451         parse_error("Incorrect group found, "
452                     "expected [Desktop Entry]",
453                     parse, error);
454         g_free(group);
455         return;
456     }
457     else {
458         if (!g) {
459             g = parse_group_new(group, NULL);
460             g_hash_table_insert(parse->group_hash, g->name, g);
461         }
462         else
463             g_free(group);
464
465         g->seen = TRUE;
466         parse->group = g;
467         /* g_print("Found group %s\n", g->name); */
468     }
469 }
470
471 static void parse_key_value(const gchar *buf, gulong len,
472                             ObtDDParse *parse, gboolean *error)
473 {
474     gulong i, keyend, valstart, eq;
475     gulong langstart, langend, countrystart, countryend, modstart, modend;
476     char *key;
477     ObtDDParseValue *val;
478     ObtDDParseLangMatch match;
479
480     /* find the end of the key */
481     for (i = 0; i < len; ++i)
482         if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
483               ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z') ||
484               ((guchar)buf[i] >= '0' && (guchar)buf[i] <= '9') ||
485               ((guchar)buf[i] == '-'))) {
486             /* not part of the key */
487             break;
488         }
489     keyend = i;
490
491     if (keyend < 1) {
492         parse_error("Empty key", parse, error);
493         return;
494     }
495
496     /* is there a language specifier? */
497     langstart = langend = countrystart = countryend = modstart = modend = 0;
498     if ((guchar)buf[i] == '[') {
499         langstart = i+1;
500         for (i = langstart; i < len; ++i)
501             if ((guchar)buf[i] == '.' || (guchar)buf[i] == '_' ||
502                 (guchar)buf[i] == '@' || (guchar)buf[i] == ']')
503             {
504                 langend = i-1;
505                 break;
506             }
507             else if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
508                        ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z'))) {
509                 parse_error("Invalid character in language", parse, error);
510                 return;
511             }
512         if ((guchar)buf[i] == '_') {
513             countrystart = i+1;
514             for (i = i+1; i < len; ++i)
515                 if ((guchar)buf[i] == '.' ||
516                     (guchar)buf[i] == '@' || (guchar)buf[i] == ']')
517                 {
518                     langend = i-1;
519                     break;
520                 }
521             else if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
522                        ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z'))) {
523                 parse_error("Invalid character in country", parse, error);
524                 return;
525             }
526         }
527         if ((guchar)buf[i] == '.') {
528             for (i = i+1; i < len; ++i)
529                 if ((guchar)buf[i] == '@' || (guchar)buf[i] == ']')
530                     break;
531             else if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
532                        ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z'))) {
533                 parse_error("Invalid character in encoding", parse, error);
534                 return;
535             }
536         }
537         if ((guchar)buf[i] == '@') {
538             modstart = i+1;
539             for (i = i+1; i < len; ++i)
540                 if ((guchar)buf[i] == ']') {
541                     modend = i-1;
542                     break;
543                 }
544             else if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
545                        ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z'))) {
546                 parse_error("Invalid character in locale modifier",
547                             parse, error);
548                 return;
549             }
550         }
551         ++i;
552     }
553
554     /* find the = character */
555     for (; i < len; ++i) {
556         if (buf[i] == '=') {
557             eq = i;
558             break;
559         }
560         else if (buf[i] != ' ') {
561             parse_error("Invalid character in key name", parse, error);
562             return;
563         }
564     }
565     if (i == len) {
566         parse_error("Key without value found", parse, error);
567         return;
568     }
569     /* find the start of the value */
570     for (i = eq+1; i < len; ++i)
571         if (buf[i] != ' ') {
572             valstart = i;
573             break;
574         }
575     if (i == len) {
576         parse_error("Empty value found", parse, error);
577         return;
578     }
579
580     if (langend < langstart)
581         match = OBT_DDPARSE_MATCH_FAIL;
582     else
583         match = OBT_DDPARSE_MATCH_NONE;
584     if (parse->language && langend >= langstart &&
585         strncmp(parse->language, buf+langstart, langend-langstart+1) == 0)
586     {
587         match = OBT_DDPARSE_MATCH_LANG;
588         if (parse->country && countryend >= countrystart &&
589             strncmp(parse->country, buf+countrystart,
590                     countryend-countrystart+1) == 0)
591             match = OBT_DDPARSE_MATCH_LANG_COUNTRY;
592
593         if (parse->modifier && modend >= modstart &&
594             strncmp(parse->modifier, buf+modstart,
595                     modend-modstart+1) == 0)
596             match += 1; /* its one up for LANG and for LANG_COUNTY */
597     }
598
599     key = g_strndup(buf, keyend);
600     if ((val = g_hash_table_lookup(parse->group->key_hash, key))) {
601         if (val->language_match >= match) {
602             /* found a better match already */
603             g_free(key);
604             return;
605         }
606     }
607     /* g_print("Found key/value %s=%s\n", key, buf+valstart); */
608     if (parse->group->value_func)
609         if (!parse->group->value_func(key, buf+valstart, match, parse, error))
610         {
611             /*parse_error("Unknown key", parse, error);*/
612             g_free(key);
613         }
614 }
615
616 static gboolean parse_file(FILE *f, ObtDDParse *parse)
617 {
618     gchar *buf = NULL;
619     gulong bytes = 0, read = 0;
620     gboolean error = FALSE;
621
622     while (!error && parse_file_line(f, &buf, &bytes, &read, parse, &error)) {
623         gulong len = strlen(buf);
624         if (buf[0] == '#' || buf[0] == '\0')
625             ; /* ignore comment lines */
626         else if (buf[0] == '[' && buf[len-1] == ']')
627             parse_group(buf, len, parse, &error);
628         else if (!parse->group)
629             /* just ignore keys outside of groups */
630             parse_error("Key found before group", parse, NULL);
631         else
632             /* ignore errors in key-value pairs and continue */
633             parse_key_value(buf, len, parse, NULL);
634         ++parse->lineno;
635     }
636
637     if (buf) g_free(buf);
638     return !error;
639 }
640
641 static gboolean parse_desktop_entry_value(gchar *key, const gchar *val,
642                                           ObtDDParseLangMatch match,
643                                           ObtDDParse *parse, gboolean *error)
644 {
645     ObtDDParseValue v, *pv;
646
647     v.language_match = match;
648
649     switch (key[0]) {
650     case 'C':
651         switch (key[1]) {
652         case 'a': /* Categories */
653             if (strcmp(key+2, "tegories")) return FALSE;
654             v.type = OBT_DDPARSE_STRINGS; break;
655         case 'o': /* Comment */
656             if (strcmp(key+2, "mment")) return FALSE;
657             v.type = OBT_DDPARSE_LOCALESTRING; break;
658         default:
659             return FALSE;
660         }
661         break;
662     case 'E': /* Exec */
663         if (strcmp(key+1, "xec")) return FALSE;
664         v.type = OBT_DDPARSE_EXEC; parse->flags |= DE_EXEC; break;
665     case 'G': /* GenericName */
666         if (strcmp(key+1, "enericName")) return FALSE;
667         v.type = OBT_DDPARSE_LOCALESTRING; break;
668     case 'I': /* Icon */
669         if (strcmp(key+1, "con")) return FALSE;
670         v.type = OBT_DDPARSE_LOCALESTRING; break;
671     case 'H': /* Hidden */
672         if (strcmp(key+1, "idden")) return FALSE;
673         v.type = OBT_DDPARSE_BOOLEAN; break;
674     case 'M': /* MimeType */
675         if (strcmp(key+1, "imeType")) return FALSE;
676         v.type = OBT_DDPARSE_STRINGS; break;
677     case 'N':
678         switch (key[1]) {
679         case 'a': /* Name */
680             if (strcmp(key+2, "me")) return FALSE;
681             v.type = OBT_DDPARSE_LOCALESTRING; parse->flags |= DE_NAME; break;
682         case 'o':
683             switch (key[2]) {
684             case 'D': /* NoDisplay */
685                 if (strcmp(key+3, "isplay")) return FALSE;
686                 v.type = OBT_DDPARSE_BOOLEAN; break;
687             case 't': /* NotShowIn */
688                 if (strcmp(key+3, "ShowIn")) return FALSE;
689                 v.type = OBT_DDPARSE_ENVIRONMENTS; break;
690             default:
691                 return FALSE;
692             }
693             break;
694         default:
695             return FALSE;
696         }
697         break;
698     case 'O': /* OnlyShowIn */
699         if (strcmp(key+1, "nlyShowIn")) return FALSE;
700         v.type = OBT_DDPARSE_ENVIRONMENTS; break;
701     case 'P': /* Path */
702         if (strcmp(key+1, "ath")) return FALSE;
703         v.type = OBT_DDPARSE_STRING; break;
704     case 'S': /* Path */
705         if (key[1] == 't' && key[2] == 'a' && key[3] == 'r' &&
706             key[4] == 't' && key[5] == 'u' && key[6] == 'p')
707             switch (key[7]) {
708             case 'N': /* StartupNotify */
709                 if (strcmp(key+8, "otify")) return FALSE;
710                 v.type = OBT_DDPARSE_BOOLEAN; break;
711             case 'W': /* StartupWMClass */
712                 if (strcmp(key+8, "MClass")) return FALSE;
713                 v.type = OBT_DDPARSE_STRING; break;
714             default:
715                 return FALSE;
716             }
717         else
718             return FALSE;
719         break;
720     case 'T':
721         switch (key[1]) {
722         case 'e': /* Terminal */
723             if (strcmp(key+2, "rminal")) return FALSE;
724             v.type = OBT_DDPARSE_BOOLEAN; break;
725         case 'r': /* TryExec */
726             if (strcmp(key+2, "yExec")) return FALSE;
727             v.type = OBT_DDPARSE_STRING; break;
728         case 'y': /* Type */
729             if (strcmp(key+2, "pe")) return FALSE;
730             v.type = OBT_DDPARSE_ENUM_TYPE; parse->flags |= DE_TYPE; break;
731         default:
732             return FALSE;
733         }
734         break;
735     case 'U': /* URL */
736         if (strcmp(key+1, "RL")) return FALSE;
737         v.type = OBT_DDPARSE_STRING; parse->flags |= DE_URL; break;
738     case 'V': /* MimeType */
739         if (strcmp(key+1, "ersion")) return FALSE;
740         v.type = OBT_DDPARSE_STRING; break;
741     default:
742         return FALSE;
743     }
744
745     if (v.language_match && !(v.type == OBT_DDPARSE_LOCALESTRING ||
746                               v.type == OBT_DDPARSE_LOCALESTRINGS))
747     {
748         parse_error("Invalid localization on key", parse, error);
749         return FALSE;
750     }
751
752     /* parse the value */
753     switch (v.type) {
754     case OBT_DDPARSE_EXEC: {
755         gchar *c, *m;
756         gboolean percent;
757         gboolean found;
758
759         v.value.string = parse_value_string(val, FALSE, FALSE, NULL,
760                                             parse, error);
761         g_assert(v.value.string);
762
763         /* an exec string can only contain one of the file/url-opening %'s */
764         percent = found = FALSE;
765         for (c = v.value.string; *c; ++c) {
766             if (percent) {
767                 switch (*c) {
768                 case 'f':
769                 case 'F':
770                 case 'u':
771                 case 'U':
772                     if (found) {
773                         m = g_strdup_printf("Malformed Exec key, "
774                                             "extraneous %%%c", *c);
775                         parse_error(m, parse, error);
776                         g_free(m);
777                     }
778                     found = TRUE;
779                     break;
780                 case 'd':
781                 case 'D':
782                 case 'n':
783                 case 'N':
784                 case 'v':
785                 case 'm':
786                     m = g_strdup_printf("Malformed Exec key, "
787                                         "uses deprecated %%%c", *c);
788                     parse_error(m, parse, NULL); /* just a warning */
789                     g_free(m);
790                     break;
791                 case 'i':
792                 case 'c':
793                 case 'k':
794                 case '%':
795                     break;
796                 default:
797                     m = g_strdup_printf("Malformed Exec key, "
798                                         "uses unknown %%%c", *c);
799                     parse_error(m, parse, NULL); /* just a warning */
800                     g_free(m);
801                 }
802                 percent = FALSE;
803             }
804             else if (*c == '%') percent = TRUE;
805         }
806         break;
807     }
808     case OBT_DDPARSE_STRING:
809         v.value.string = parse_value_string(val, FALSE, FALSE, NULL,
810                                             parse, error);
811         g_assert(v.value.string);
812         break;
813     case OBT_DDPARSE_LOCALESTRING:
814         v.value.string = parse_value_string(val, TRUE, FALSE, NULL,
815                                             parse, error);
816         g_assert(v.value.string);
817         break;
818     case OBT_DDPARSE_STRINGS:
819         v.value.strings.a = parse_value_strings(val, FALSE, &v.value.strings.n,
820                                                 parse, error);
821         g_assert(v.value.strings.a);
822         g_assert(v.value.strings.n);
823         break;
824     case OBT_DDPARSE_LOCALESTRINGS:
825         v.value.strings.a = parse_value_strings(val, TRUE, &v.value.strings.n,
826                                                 parse, error);
827         g_assert(v.value.strings.a);
828         g_assert(v.value.strings.n);
829         break;
830     case OBT_DDPARSE_BOOLEAN:
831         v.value.boolean = parse_value_boolean(val, parse, error);
832         break;
833     case OBT_DDPARSE_NUMERIC:
834         v.value.numeric = parse_value_numeric(val, parse, error);
835         break;
836     case OBT_DDPARSE_ENUM_TYPE:
837         if (val[0] == 'A' && strcmp(val+1, "pplication") == 0) {
838             v.value.enumerable = OBT_LINK_TYPE_APPLICATION;
839             parse->flags |= DE_TYPE_APPLICATION;
840         }
841         else if (val[0] == 'L' && strcmp(val+1, "ink") == 0) {
842             v.value.enumerable = OBT_LINK_TYPE_URL;
843             parse->flags |= DE_TYPE_LINK;
844         }
845         else if (val[0] == 'D' && strcmp(val+1, "irectory") == 0)
846             v.value.enumerable = OBT_LINK_TYPE_DIRECTORY;
847         else {
848             parse_error("Unknown Type", parse, error);
849             return FALSE;
850         }
851         break;
852     case OBT_DDPARSE_ENVIRONMENTS:
853         v.value.environments = parse_value_environments(val, parse, error);
854         break;
855     default:
856         g_assert_not_reached();
857     }
858
859     pv = g_slice_new(ObtDDParseValue);
860     *pv = v;
861     g_hash_table_insert(parse->group->key_hash, key, pv);
862     return TRUE;
863 }
864
865 GHashTable* obt_ddparse_file(const gchar *filename,
866                              const gchar *language,
867                              const gchar *country,
868                              const gchar *modifier)
869 {
870     ObtDDParse parse;
871     ObtDDParseGroup *desktop_entry;
872     FILE *f;
873     gboolean success;
874     gchar *fs_filename;
875
876     if (!g_utf8_validate(filename, -1, NULL)) {
877         g_warning("Filename contains bad utf8: %s", filename);
878         return NULL;
879     }
880
881     parse.filename = filename;
882     parse.language = language;
883     parse.country = country;
884     parse.modifier = modifier;
885     parse.lineno = 0;
886     parse.group = NULL;
887     parse.group_hash = g_hash_table_new_full(g_str_hash,
888                                              g_str_equal,
889                                              NULL,
890                                              (GDestroyNotify)parse_group_free);
891
892     /* set up the groups (there's only one right now) */
893     desktop_entry = parse_group_new(g_strdup("Desktop Entry"),
894                                     parse_desktop_entry_value);
895     g_hash_table_insert(parse.group_hash, desktop_entry->name, desktop_entry);
896
897     success = FALSE;
898     fs_filename = g_filename_from_utf8(filename, -1, NULL, NULL, NULL);
899     if ((f = fopen(fs_filename, "r"))) {
900         parse.lineno = 1;
901         parse.flags = 0;
902         if ((success = parse_file(f, &parse))) {
903             /* check that required keys exist */
904
905             if (!(parse.flags & DE_TYPE)) {
906                 g_warning("Missing Type key in %s", parse.filename);
907                 success = FALSE;
908             }
909             if (!(parse.flags & DE_NAME)) {
910                 g_warning("Missing Name key in %s", parse.filename);
911                 success = FALSE;
912             }
913             if (parse.flags & DE_TYPE_APPLICATION &&
914                 !(parse.flags & DE_EXEC))
915             {
916                 g_warning("Missing Exec key for Application in %s",
917                           parse.filename);
918                 success = FALSE;
919             }
920             else if (parse.flags & DE_TYPE_LINK && !(parse.flags & DE_URL))
921             {
922                 g_warning("Missing URL key for Link in %s", parse.filename);
923                 success = FALSE;
924             }
925         }
926         fclose(f);
927     }
928     g_free(fs_filename);
929     if (!success) {
930         g_hash_table_destroy(parse.group_hash);
931         parse.group_hash = NULL;
932     }
933     return parse.group_hash;
934 }
935
936 GHashTable* obt_ddparse_group_keys(ObtDDParseGroup *g)
937 {
938     return g->key_hash;
939 }
940
941 gchar* obt_ddparse_file_to_id(const gchar *filename)
942 {
943     gint len;
944     const gchar *in;
945     gchar *out, *out_start;
946     gboolean sep;
947
948     if (!g_utf8_validate(filename, -1, NULL)) {
949         g_warning("Filename contains bad utf8: %s", filename);
950         return NULL;
951     }
952
953     len = strlen(filename) - 8;  /* 8 = strlen(".desktop") */
954     g_assert(strcmp(filename+len, ".desktop") == 0);
955
956     out_start = out = g_new(char, len+1);
957     sep = TRUE;
958     for (in = filename; in < filename + len; ) {
959         gchar *next;
960
961         if (*in == '/') {
962             /* path separators becomes dashes */
963             if (!sep) {
964                 *out = '-';
965                 ++out;
966             }
967             sep = TRUE;
968             ++in;
969         }
970         else {
971             /* everything else is copied as is */
972             next = g_utf8_next_char(in);
973             while (in < next) {
974                 *out = *in;
975                 ++out;
976                 ++in;
977             }
978             sep = FALSE;
979         }
980     }
981     *out = '\0';
982     return out_start;
983 }