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