]> icculus.org git repositories - dana/openbox.git/blob - obt/ddfile.c
add an empty function to parse the Desktop Entry group in a .desktop file
[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 ObtDDParseGroup* group_new(gchar *name, ObtDDParseGroupFunc f)
88 {
89     ObtDDParseGroup *g = g_slice_new(ObtDDParseGroup);
90     g->name = name;
91     g->func = f;
92     g->seen = FALSE;
93     g->key_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
94                                         g_free, g_free);
95     return g;
96 }
97
98 static void group_free(ObtDDParseGroup *g)
99 {
100     g_free(g->name);
101     g_hash_table_destroy(g->key_hash);
102     g_slice_free(ObtDDParseGroup, g);
103 }
104
105 /* Displays a warning message including the file name and line number, and
106    sets the boolean @error to true if it points to a non-NULL address.
107 */
108 static void parse_error(const gchar *m, const ObtDDParse *const parse,
109                         gboolean *error)
110 {
111     if (!parse->filename)
112         g_warning("%s at line %lu of input", m, parse->lineno);
113     else
114         g_warning("%s at line %lu of file %s",
115                   m, parse->lineno, parse->filename);
116     if (error) *error = TRUE;
117 }
118
119 /* reads an input string, strips out invalid stuff, and parses
120    backslash-stuff */
121 static gchar* parse_string(const gchar *in, gboolean locale,
122                            const ObtDDParse *const parse,
123                            gboolean *error)
124 {
125     const gint bytes = strlen(in);
126     gboolean backslash;
127     gchar *out, *o;
128     const gchar *end, *i;
129
130     g_return_val_if_fail(in != NULL, NULL);
131
132     if (!locale) {
133         end = in + bytes;
134         for (i = in; i < end; ++i) {
135             if ((guchar)*i > 126 || (guchar)*i < 32) {
136                 /* non-control character ascii */
137                 end = i;
138                 parse_error("Invalid bytes in string", parse, error);
139                 break;
140             }
141         }
142     }
143     else if (!g_utf8_validate(in, bytes, &end))
144         parse_error("Invalid bytes in localestring", parse, error);
145
146     out = g_new(char, bytes + 1);
147     i = in; o = out;
148     backslash = FALSE;
149     while (i < end) {
150         const gchar *next = locale ? g_utf8_find_next_char(i, end) : i+1;
151         if (backslash) {
152             switch(*i) {
153             case 's': *o++ = ' '; break;
154             case 'n': *o++ = '\n'; break;
155             case 't': *o++ = '\t'; break;
156             case 'r': *o++ = '\r'; break;
157             case '\\': *o++ = '\\'; break;
158             default:
159                 parse_error((locale ?
160                              "Invalid escape sequence in localestring" :
161                              "Invalid escape sequence in string"),
162                             parse, error);
163             }
164             backslash = FALSE;
165         }
166         else if (*i == '\\')
167             backslash = TRUE;
168         else if ((guchar)*i >= 127 || (guchar)*i < 32) {
169             /* avoid ascii control characters */
170             parse_error("Found control character in string", parse, error);
171             break;
172         }
173         else {
174             memcpy(o, i, next-i);
175             o += next-i;
176         }
177         i = next;
178     }
179     *o = '\0';
180     return o;
181 }
182
183 static gboolean parse_bool(const gchar *in, const ObtDDParse *const parse,
184                            gboolean *error)
185 {
186     if (strcmp(in, "true") == 0)
187         return TRUE;
188     else if (strcmp(in, "false") != 0)
189         parse_error("Invalid boolean value", parse, error);
190     return FALSE;
191 }
192
193 static float parse_numeric(const gchar *in, const ObtDDParse *const parse,
194     gboolean *error)
195 {
196     float out = 0;
197     if (sscanf(in, "%f", &out) == 0)
198         parse_error("Invalid numeric value", parse, error);
199     return out;
200 }
201
202 gboolean parse_file_line(FILE *f, gchar **buf, gulong *size, gulong *read,
203                          ObtDDParse *parse, gboolean *error)
204 {
205     const gulong BUFMUL = 80;
206     size_t ret;
207     gulong i, null;
208
209     if (*size == 0) {
210         g_assert(*read == 0);
211         *size = BUFMUL;
212         *buf = g_new(char, *size);
213     }
214
215     /* remove everything up to a null zero already in the buffer and shift
216        the rest to the front */
217     null = *size;
218     for (i = 0; i < *read; ++i) {
219         if (null < *size)
220             (*buf)[i-null-1] = (*buf)[i];
221         else if ((*buf)[i] == '\0')
222             null = i;
223     }
224     if (null < *size)
225         *read -= null + 1;
226
227     /* is there already a newline in the buffer? */
228     for (i = 0; i < *read; ++i)
229         if ((*buf)[i] == '\n') {
230             /* turn it into a null zero and done */
231             (*buf)[i] = '\0';
232             return TRUE;
233         }
234
235     /* we need to read some more to find a newline */
236     while (TRUE) {
237         gulong eol;
238         gchar *newread;
239
240         newread = *buf + *read;
241         ret = fread(newread, sizeof(char), *size-*read, f);
242         if (ret < *size - *read && !feof(f)) {
243             parse_error("Error reading", parse, error);
244             return FALSE;
245         }
246         *read += ret;
247
248         /* strip out null zeros in the input and look for an endofline */
249         null = 0;
250         eol = *size;
251         for (i = newread-*buf; i < *read; ++i) {
252             if (null > 0)
253                 (*buf)[i] = (*buf)[i+null];
254             if ((*buf)[i] == '\0') {
255                 ++null;
256                 --(*read);
257                 --i; /* try again */
258             }
259             else if ((*buf)[i] == '\n' && eol == *size) {
260                 eol = i;
261                 /* turn it into a null zero */
262                 (*buf)[i] = '\0';
263             }
264         }
265
266         if (eol != *size)
267             /* found an endofline, done */
268             break;
269         else if (feof(f) && *read < *size) {
270             /* found the endoffile, done (if there is space) */
271             if (*read > 0) {
272                 /* stick a null zero on if there is test on the last line */
273                 (*buf)[(*read)++] = '\0';
274             }
275             break;
276         }
277         else {
278             /* read more */
279             size += BUFMUL;
280             *buf = g_renew(char, *buf, *size);
281         }
282     }
283     return *read > 0;
284 }
285
286 static void parse_group(const gchar *buf, gulong len,
287                         ObtDDParse *parse, gboolean *error)
288 {
289     ObtDDParseGroup *g;
290     gchar *group;
291     gulong i;
292
293     /* get the group name */
294     group = g_strndup(buf+1, len-2);
295     for (i = 0; i < len-2; ++i)
296         if ((guchar)group[i] < 32 || (guchar)group[i] >= 127) {
297             /* valid ASCII only */
298             parse_error("Invalid character found", parse, NULL);
299             group[i] = '\0'; /* stopping before this character */
300             break;
301         }
302
303     /* make sure it's a new group */
304     g = g_hash_table_lookup(parse->group_hash, group);
305     if (g && g->seen) {
306         parse_error("Duplicate group found", parse, error);
307         g_free(group);
308         return;
309     }
310     /* if it's the first group, make sure it's named Desktop Entry */
311     else if (!parse->group && strcmp(group, "Desktop Entry") != 0)
312     {
313         parse_error("Incorrect group found, "
314                     "expected [Desktop Entry]",
315                     parse, error);
316         g_free(group);
317         return;
318     }
319     else {
320         if (!g) {
321             g = group_new(group, NULL);
322             g_hash_table_insert(parse->group_hash, g->name, g);
323         }
324         else
325             g_free(group);
326
327         g->seen = TRUE;
328         parse->group = g;
329         g_print("Found group %s\n", g->name);
330     }
331 }
332
333 static void parse_key_value(const gchar *buf, gulong len,
334                             ObtDDParse *parse, gboolean *error)
335 {
336     gulong i, keyend, valstart, eq;
337     char *key, *val;
338
339     /* find the end of the key */
340     for (i = 0; i < len; ++i)
341         if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
342               ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z') ||
343               ((guchar)buf[i] >= '0' && (guchar)buf[i] <= '9') ||
344               ((guchar)buf[i] == '-'))) {
345             /* not part of the key */
346             keyend = i;
347             break;
348         }
349     if (keyend < 1) {
350         parse_error("Empty key", parse, error);
351         return;
352     }
353     /* find the = character */
354     for (i = keyend; i < len; ++i) {
355         if (buf[i] == '=') {
356             eq = i;
357             break;
358         }
359         else if (buf[i] != ' ') {
360             parse_error("Invalid character in key name", parse, error);
361             return ;
362         }
363     }
364     if (i == len) {
365         parse_error("Key without value found", parse, error);
366         return;
367     }
368     /* find the start of the value */
369     for (i = eq+1; i < len; ++i)
370         if (buf[i] != ' ') {
371             valstart = i;
372             break;
373         }
374     if (i == len) {
375         parse_error("Empty value found", parse, error);
376         return;
377     }
378
379     key = g_strndup(buf, keyend);
380     val = g_strndup(buf+valstart, len-valstart);
381     if (g_hash_table_lookup(parse->group->key_hash, key)) {
382         parse_error("Duplicate key found", parse, error);
383         g_free(key);
384         g_free(val);
385         return;
386     }
387     g_hash_table_insert(parse->group->key_hash, key, val);
388     g_print("Found key/value %s=%s.\n", key, val);
389 }
390
391 void foreach_group(gpointer pkey, gpointer pvalue, gpointer user_data)
392 {
393     gchar *name = pkey;
394     ObtDDParseGroup *g = pvalue;
395     if (g->func) g->func(name, g->key_hash);
396 }
397
398 static gboolean parse_file(ObtDDFile *dd, FILE *f, ObtDDParse *parse)
399 {
400     gchar *buf = NULL;
401     gulong bytes = 0, read = 0;
402     gboolean error = FALSE;
403
404     while (!error && parse_file_line(f, &buf, &bytes, &read, parse, &error)) {
405         /* XXX use the string in buf */
406         gulong len = strlen(buf);
407         if (buf[0] == '#' || buf[0] == '\0')
408             ; /* ignore comment lines */
409         else if (buf[0] == '[' && buf[len-1] == ']')
410             parse_group(buf, len, parse, &error);
411         else if (!parse->group)
412             /* just ignore keys outside of groups */
413             parse_error("Key found before group", parse, NULL);
414         else
415             /* ignore errors in key-value pairs and continue */
416             parse_key_value(buf, len, parse, NULL);
417         ++parse->lineno;
418     }
419
420     g_hash_table_foreach(parse->group_hash, foreach_group, NULL);
421
422     if (buf) g_free(buf);
423     return !error;
424 }
425
426 static void parse_group_desktop_entry(const gchar *group,
427                                       GHashTable *keys)
428 {
429     g_print("Parsing group %s\n", group);
430 }
431
432 ObtDDFile* obt_ddfile_new_from_file(const gchar *name, GSList *paths)
433 {
434     ObtDDFile *dd;
435     ObtDDParse parse;
436     ObtDDParseGroup *desktop_entry;
437     GSList *it;
438     FILE *f;
439     gboolean success;
440
441     dd = g_slice_new(ObtDDFile);
442     dd->ref = 1;
443
444     parse.filename = NULL;
445     parse.lineno = 0;
446     parse.group = NULL;
447     parse.group_hash = g_hash_table_new_full(g_str_hash,
448                                              g_str_equal,
449                                              NULL,
450                                              (GDestroyNotify)group_free);
451
452     /* set up the groups (there's only one right now) */
453     desktop_entry = group_new(g_strdup("Desktop Entry"),
454                               parse_group_desktop_entry);
455     g_hash_table_insert(parse.group_hash, desktop_entry->name, desktop_entry);
456
457     success = FALSE;
458     for (it = paths; it && !success; it = g_slist_next(it)) {
459         gchar *path = g_strdup_printf("%s/%s", (char*)it->data, name);
460         if ((f = fopen(path, "r"))) {
461             parse.filename = path;
462             parse.lineno = 1;
463             success = parse_file(dd, f, &parse);
464             fclose(f);
465         }
466         g_free(path);
467     }
468     if (!success) {
469         obt_ddfile_unref(dd);
470         dd = NULL;
471     }
472
473     g_hash_table_destroy(parse.group_hash);
474
475     return dd;
476 }
477
478 void obt_ddfile_ref(ObtDDFile *dd)
479 {
480     ++dd->ref;
481 }
482
483 void obt_ddfile_unref(ObtDDFile *dd)
484 {
485     if (--dd->ref < 1) {
486         g_slice_free(ObtDDFile, dd);
487     }
488 }