save key values based on their value, but we dont know what values they have yet
[dana/openbox.git] / obt / ddfile.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    obt/ddfile.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/ddfile.h"
20 #include <glib.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 typedef struct _ObtDDParseGroup ObtDDParseGroup;
30
31 typedef void (*ObtDDParseGroupFunc)(gchar *key, const gchar *val,
32                                     ObtDDParse *parse, gboolean *error);
33
34 struct _ObtDDParseGroup {
35     gchar *name;
36     gboolean seen;
37     ObtDDParseGroupFunc key_func;
38     /* the key is a string (a key inside the group in the .desktop).
39        the value is an ObtDDParseValue */
40     GHashTable *key_hash;
41 };
42
43 struct _ObtDDParse {
44     gchar *filename;
45     gulong lineno;
46     ObtDDParseGroup *group;
47     /* the key is a group name, the value is a ObtDDParseGroup */
48     GHashTable *group_hash;
49 };
50
51 typedef enum {
52     DATA_STRING,
53     DATA_LOCALESTRING,
54     DATA_STRINGS,
55     DATA_LOCALESTRINGS,
56     DATA_BOOLEAN,
57     DATA_NUMERIC,
58     NUM_DATA_TYPES
59 } ObtDDDataType;
60
61 typedef struct _ObtDDParseValue {
62     ObtDDDataType type;
63     union _ObtDDParseValueValue {
64         gchar *string;
65         struct _ObtDDParseValueStrings {
66             gchar *s;
67             gulong n;
68         } strings;
69         gboolean boolean;
70         gfloat numeric;
71     } value;
72 } ObtDDParseValue;
73
74 struct _ObtDDFile {
75     guint ref;
76
77     ObtDDFileType type;
78     gchar *name; /*!< Specific name for the object (eg Firefox) */
79     gchar *generic; /*!< Generic name for the object (eg Web Browser) */
80     gchar *comment; /*!< Comment/description to display for the object */
81     gchar *icon; /*!< Name/path for an icon for the object */
82
83     union _ObtDDFileData {
84         struct _ObtDDFileApp {
85             gchar *exec; /*!< Executable to run for the app */
86             gchar *wdir; /*!< Working dir to run the app in */
87             gboolean term; /*!< Run the app in a terminal or not */
88             ObtDDFileAppOpen open;
89
90             /* XXX gchar**? or something better, a mime struct.. maybe
91                glib has something i can use. */
92             gchar **mime; /*!< Mime types the app can open */
93
94             ObtDDFileAppStartup startup;
95             gchar *startup_wmclass;
96         } app;
97         struct _ObtDDFileLink {
98             gchar *url;
99         } link;
100         struct _ObtDDFileDir {
101         } dir;
102     } d;
103 };
104
105 static void value_free(ObtDDParseValue *v)
106 {
107     switch (v->type) {
108     case DATA_STRING:
109     case DATA_LOCALESTRING:
110         g_free(v->value.string); break;
111     case DATA_STRINGS:
112     case DATA_LOCALESTRINGS:
113         g_free(v->value.strings.s);
114         v->value.strings.n = 0;
115         break;
116     case DATA_BOOLEAN:
117         break;
118     case DATA_NUMERIC:
119         break;
120     default:
121         g_assert_not_reached();
122     }
123     g_slice_free(ObtDDParseValue, v);
124 }
125
126 static ObtDDParseGroup* group_new(gchar *name, ObtDDParseGroupFunc f)
127 {
128     ObtDDParseGroup *g = g_slice_new(ObtDDParseGroup);
129     g->name = name;
130     g->key_func = f;
131     g->seen = FALSE;
132     g->key_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
133                                         g_free, (GDestroyNotify)value_free);
134     return g;
135 }
136
137 static void group_free(ObtDDParseGroup *g)
138 {
139     g_free(g->name);
140     g_hash_table_destroy(g->key_hash);
141     g_slice_free(ObtDDParseGroup, g);
142 }
143
144 /* Displays a warning message including the file name and line number, and
145    sets the boolean @error to true if it points to a non-NULL address.
146 */
147 static void parse_error(const gchar *m, const ObtDDParse *const parse,
148                         gboolean *error)
149 {
150     if (!parse->filename)
151         g_warning("%s at line %lu of input", m, parse->lineno);
152     else
153         g_warning("%s at line %lu of file %s",
154                   m, parse->lineno, parse->filename);
155     if (error) *error = TRUE;
156 }
157
158 /* reads an input string, strips out invalid stuff, and parses
159    backslash-stuff
160    if @nstrings is not NULL, then it splits the output string at ';'
161    characters.  they are all returned in the same string with null zeros
162    between them, @nstrings is set to the number of such strings.
163  */
164 static gchar* parse_string(const gchar *in,
165                            gboolean locale,
166                            gulong *nstrings,
167                            const ObtDDParse *const parse,
168                            gboolean *error)
169 {
170     const gint bytes = strlen(in);
171     gboolean backslash;
172     gchar *out, *o;
173     const gchar *end, *i;
174
175     g_return_val_if_fail(in != NULL, NULL);
176
177     if (!locale) {
178         end = in + bytes;
179         for (i = in; i < end; ++i) {
180             if ((guchar)*i > 126 || (guchar)*i < 32) {
181                 /* non-control character ascii */
182                 end = i;
183                 parse_error("Invalid bytes in string", parse, error);
184                 break;
185             }
186         }
187     }
188     else if (!g_utf8_validate(in, bytes, &end))
189         parse_error("Invalid bytes in localestring", parse, error);
190
191     if (nstrings) *nstrings = 1;
192
193     out = g_new(char, bytes + 1);
194     i = in; o = out;
195     backslash = FALSE;
196     while (i < end) {
197         const gchar *next = locale ? g_utf8_find_next_char(i, end) : i+1;
198         if (backslash) {
199             switch(*i) {
200             case 's': *o++ = ' '; break;
201             case 'n': *o++ = '\n'; break;
202             case 't': *o++ = '\t'; break;
203             case 'r': *o++ = '\r'; break;
204             case ';': *o++ = ';'; break;
205             case '\\': *o++ = '\\'; break;
206             default:
207                 parse_error((locale ?
208                              "Invalid escape sequence in localestring" :
209                              "Invalid escape sequence in string"),
210                             parse, error);
211             }
212             backslash = FALSE;
213         }
214         else if (*i == '\\')
215             backslash = TRUE;
216         else if (*i == ';' && nstrings) {
217             ++nstrings;
218             *o = '\0';
219         }
220         else if ((guchar)*i >= 127 || (guchar)*i < 32) {
221             /* avoid ascii control characters */
222             parse_error("Found control character in string", parse, error);
223             break;
224         }
225         else {
226             memcpy(o, i, next-i);
227             o += next-i;
228         }
229         i = next;
230     }
231     *o = '\0';
232     return o;
233 }
234
235 static gboolean parse_bool(const gchar *in, const ObtDDParse *const parse,
236                            gboolean *error)
237 {
238     if (strcmp(in, "true") == 0)
239         return TRUE;
240     else if (strcmp(in, "false") != 0)
241         parse_error("Invalid boolean value", parse, error);
242     return FALSE;
243 }
244
245 static gfloat parse_numeric(const gchar *in, const ObtDDParse *const parse,
246     gboolean *error)
247 {
248     gfloat out = 0;
249     if (sscanf(in, "%f", &out) == 0)
250         parse_error("Invalid numeric value", parse, error);
251     return out;
252 }
253
254 static void parse_group_desktop_entry(gchar *key, const gchar *val,
255                                       ObtDDParse *parse, gboolean *error)
256 {
257     ObtDDParseValue v, *pv;
258
259     /* figure out value type */
260     v.type = NUM_DATA_TYPES;
261
262     /* parse the value */
263     
264     switch (v.type) {
265     case DATA_STRING:
266         v.value.string = parse_string(val, FALSE, NULL, parse, error);
267         g_assert(v.value.string);
268         break;
269     case DATA_LOCALESTRING:
270         v.value.string = parse_string(val, TRUE, NULL, parse, error);
271         g_assert(v.value.string);
272         break;
273     case DATA_STRINGS:
274         v.value.strings.s = parse_string(val, FALSE, &v.value.strings.n,
275                                          parse, error);
276         g_assert(v.value.strings.s);
277         g_assert(v.value.strings.n);
278         break;
279     case DATA_LOCALESTRINGS:
280         v.value.strings.s = parse_string(val, TRUE, &v.value.strings.n,
281                                          parse, error);
282         g_assert(v.value.strings.s);
283         g_assert(v.value.strings.n);
284         break;
285     case DATA_BOOLEAN:
286         v.value.boolean = parse_bool(val, parse, error);
287         break;
288     case DATA_NUMERIC:
289         v.value.numeric = parse_numeric(val, parse, error);
290         break;
291     default:
292         g_assert_not_reached();
293     }
294
295     pv = g_slice_new(ObtDDParseValue);
296     *pv = v;
297     g_hash_table_insert(parse->group->key_hash, key, pv);
298 }
299
300 static gboolean parse_file_line(FILE *f, gchar **buf,
301                                 gulong *size, gulong *read,
302                                 ObtDDParse *parse, gboolean *error)
303 {
304     const gulong BUFMUL = 80;
305     size_t ret;
306     gulong i, null;
307
308     if (*size == 0) {
309         g_assert(*read == 0);
310         *size = BUFMUL;
311         *buf = g_new(char, *size);
312     }
313
314     /* remove everything up to a null zero already in the buffer and shift
315        the rest to the front */
316     null = *size;
317     for (i = 0; i < *read; ++i) {
318         if (null < *size)
319             (*buf)[i-null-1] = (*buf)[i];
320         else if ((*buf)[i] == '\0')
321             null = i;
322     }
323     if (null < *size)
324         *read -= null + 1;
325
326     /* is there already a newline in the buffer? */
327     for (i = 0; i < *read; ++i)
328         if ((*buf)[i] == '\n') {
329             /* turn it into a null zero and done */
330             (*buf)[i] = '\0';
331             return TRUE;
332         }
333
334     /* we need to read some more to find a newline */
335     while (TRUE) {
336         gulong eol;
337         gchar *newread;
338
339         newread = *buf + *read;
340         ret = fread(newread, sizeof(char), *size-*read, f);
341         if (ret < *size - *read && !feof(f)) {
342             parse_error("Error reading", parse, error);
343             return FALSE;
344         }
345         *read += ret;
346
347         /* strip out null zeros in the input and look for an endofline */
348         null = 0;
349         eol = *size;
350         for (i = newread-*buf; i < *read; ++i) {
351             if (null > 0)
352                 (*buf)[i] = (*buf)[i+null];
353             if ((*buf)[i] == '\0') {
354                 ++null;
355                 --(*read);
356                 --i; /* try again */
357             }
358             else if ((*buf)[i] == '\n' && eol == *size) {
359                 eol = i;
360                 /* turn it into a null zero */
361                 (*buf)[i] = '\0';
362             }
363         }
364
365         if (eol != *size)
366             /* found an endofline, done */
367             break;
368         else if (feof(f) && *read < *size) {
369             /* found the endoffile, done (if there is space) */
370             if (*read > 0) {
371                 /* stick a null zero on if there is test on the last line */
372                 (*buf)[(*read)++] = '\0';
373             }
374             break;
375         }
376         else {
377             /* read more */
378             size += BUFMUL;
379             *buf = g_renew(char, *buf, *size);
380         }
381     }
382     return *read > 0;
383 }
384
385 static void parse_group(const gchar *buf, gulong len,
386                         ObtDDParse *parse, gboolean *error)
387 {
388     ObtDDParseGroup *g;
389     gchar *group;
390     gulong i;
391
392     /* get the group name */
393     group = g_strndup(buf+1, len-2);
394     for (i = 0; i < len-2; ++i)
395         if ((guchar)group[i] < 32 || (guchar)group[i] >= 127) {
396             /* valid ASCII only */
397             parse_error("Invalid character found", parse, NULL);
398             group[i] = '\0'; /* stopping before this character */
399             break;
400         }
401
402     /* make sure it's a new group */
403     g = g_hash_table_lookup(parse->group_hash, group);
404     if (g && g->seen) {
405         parse_error("Duplicate group found", parse, error);
406         g_free(group);
407         return;
408     }
409     /* if it's the first group, make sure it's named Desktop Entry */
410     else if (!parse->group && strcmp(group, "Desktop Entry") != 0)
411     {
412         parse_error("Incorrect group found, "
413                     "expected [Desktop Entry]",
414                     parse, error);
415         g_free(group);
416         return;
417     }
418     else {
419         if (!g) {
420             g = group_new(group, NULL);
421             g_hash_table_insert(parse->group_hash, g->name, g);
422         }
423         else
424             g_free(group);
425
426         g->seen = TRUE;
427         parse->group = g;
428         g_print("Found group %s\n", g->name);
429     }
430 }
431
432 static void parse_key_value(const gchar *buf, gulong len,
433                             ObtDDParse *parse, gboolean *error)
434 {
435     gulong i, keyend, valstart, eq;
436     char *key;
437
438     /* find the end of the key */
439     for (i = 0; i < len; ++i)
440         if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
441               ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z') ||
442               ((guchar)buf[i] >= '0' && (guchar)buf[i] <= '9') ||
443               ((guchar)buf[i] == '-'))) {
444             /* not part of the key */
445             keyend = i;
446             break;
447         }
448     if (keyend < 1) {
449         parse_error("Empty key", parse, error);
450         return;
451     }
452     /* find the = character */
453     for (i = keyend; i < len; ++i) {
454         if (buf[i] == '=') {
455             eq = i;
456             break;
457         }
458         else if (buf[i] != ' ') {
459             parse_error("Invalid character in key name", parse, error);
460             return ;
461         }
462     }
463     if (i == len) {
464         parse_error("Key without value found", parse, error);
465         return;
466     }
467     /* find the start of the value */
468     for (i = eq+1; i < len; ++i)
469         if (buf[i] != ' ') {
470             valstart = i;
471             break;
472         }
473     if (i == len) {
474         parse_error("Empty value found", parse, error);
475         return;
476     }
477
478     key = g_strndup(buf, keyend);
479     if (g_hash_table_lookup(parse->group->key_hash, key)) {
480         parse_error("Duplicate key found", parse, error);
481         g_free(key);
482         return;
483     }
484     g_print("Found key/value %s=%s.\n", key, buf+valstart);
485     if (parse->group->key_func)
486         parse->group->key_func(key, buf+valstart, parse, error);
487 }
488
489 static gboolean parse_file(ObtDDFile *dd, FILE *f, ObtDDParse *parse)
490 {
491     gchar *buf = NULL;
492     gulong bytes = 0, read = 0;
493     gboolean error = FALSE;
494
495     while (!error && parse_file_line(f, &buf, &bytes, &read, parse, &error)) {
496         /* XXX use the string in buf */
497         gulong len = strlen(buf);
498         if (buf[0] == '#' || buf[0] == '\0')
499             ; /* ignore comment lines */
500         else if (buf[0] == '[' && buf[len-1] == ']')
501             parse_group(buf, len, parse, &error);
502         else if (!parse->group)
503             /* just ignore keys outside of groups */
504             parse_error("Key found before group", parse, NULL);
505         else
506             /* ignore errors in key-value pairs and continue */
507             parse_key_value(buf, len, parse, NULL);
508         ++parse->lineno;
509     }
510
511     if (buf) g_free(buf);
512     return !error;
513 }
514
515 ObtDDFile* obt_ddfile_new_from_file(const gchar *name, GSList *paths)
516 {
517     ObtDDFile *dd;
518     ObtDDParse parse;
519     ObtDDParseGroup *desktop_entry;
520     GSList *it;
521     FILE *f;
522     gboolean success;
523
524     dd = g_slice_new(ObtDDFile);
525     dd->ref = 1;
526
527     parse.filename = NULL;
528     parse.lineno = 0;
529     parse.group = NULL;
530     parse.group_hash = g_hash_table_new_full(g_str_hash,
531                                              g_str_equal,
532                                              NULL,
533                                              (GDestroyNotify)group_free);
534
535     /* set up the groups (there's only one right now) */
536     desktop_entry = group_new(g_strdup("Desktop Entry"),
537                               parse_group_desktop_entry);
538     g_hash_table_insert(parse.group_hash, desktop_entry->name, desktop_entry);
539
540     success = FALSE;
541     for (it = paths; it && !success; it = g_slist_next(it)) {
542         gchar *path = g_strdup_printf("%s/%s", (char*)it->data, name);
543         if ((f = fopen(path, "r"))) {
544             parse.filename = path;
545             parse.lineno = 1;
546             success = parse_file(dd, f, &parse);
547             fclose(f);
548         }
549         g_free(path);
550     }
551     if (!success) {
552         obt_ddfile_unref(dd);
553         dd = NULL;
554     }
555
556     g_hash_table_destroy(parse.group_hash);
557
558     return dd;
559 }
560
561 void obt_ddfile_ref(ObtDDFile *dd)
562 {
563     ++dd->ref;
564 }
565
566 void obt_ddfile_unref(ObtDDFile *dd)
567 {
568     if (--dd->ref < 1) {
569         g_slice_free(ObtDDFile, dd);
570     }
571 }