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