]> icculus.org git repositories - dana/openbox.git/blob - obt/ddparse.c
Add a new ObtLink type, which is going to be a generalization of a .desktop entry.
[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 #ifdef HAVE_STRING_H
21 #include <string.h>
22 #endif
23 #ifdef HAVE_STDIO_H
24 #include <stdio.h>
25 #endif
26
27 typedef struct _ObtDDParse ObtDDParse;
28
29 /* Parses the value and adds it to the group's key_hash, with the given
30    key */
31 typedef void (*ObtDDParseValueFunc)(gchar *key, const gchar *val,
32                                     ObtDDParse *parse, gboolean *error);
33
34
35 struct _ObtDDParse {
36     gchar *filename;
37     gulong lineno;
38     ObtDDParseGroup *group;
39     /* the key is a group name, the value is a ObtDDParseGroup */
40     GHashTable *group_hash;
41 };
42
43 struct _ObtDDParseGroup {
44     gchar *name;
45     gboolean seen;
46     ObtDDParseValueFunc value_func;
47     /* the key is a string (a key inside the group in the .desktop).
48        the value is an ObtDDParseValue */
49     GHashTable *key_hash;
50 };
51
52 /* Displays a warning message including the file name and line number, and
53    sets the boolean @error to true if it points to a non-NULL address.
54 */
55 static void parse_error(const gchar *m, const ObtDDParse *const parse,
56                         gboolean *error)
57 {
58     if (!parse->filename)
59         g_warning("%s at line %lu of input", m, parse->lineno);
60     else
61         g_warning("%s at line %lu of file %s",
62                   m, parse->lineno, parse->filename);
63     if (error) *error = TRUE;
64 }
65
66 static void parse_value_free(ObtDDParseValue *v)
67 {
68     switch (v->type) {
69     case OBT_DDPARSE_STRING:
70     case OBT_DDPARSE_LOCALESTRING:
71         g_free(v->value.string); break;
72     case OBT_DDPARSE_STRINGS:
73     case OBT_DDPARSE_LOCALESTRINGS:
74         g_free(v->value.strings.s);
75         v->value.strings.n = 0;
76         break;
77     case OBT_DDPARSE_BOOLEAN:
78         break;
79     case OBT_DDPARSE_NUMERIC:
80         break;
81     default:
82         g_assert_not_reached();
83     }
84     g_slice_free(ObtDDParseValue, v);
85 }
86
87 static ObtDDParseGroup* parse_group_new(gchar *name, ObtDDParseValueFunc f)
88 {
89     ObtDDParseGroup *g = g_slice_new(ObtDDParseGroup);
90     g->name = name;
91     g->value_func = f;
92     g->seen = FALSE;
93     g->key_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
94                                         g_free,
95                                         (GDestroyNotify)parse_value_free);
96     return g;
97 }
98
99 static void parse_group_free(ObtDDParseGroup *g)
100 {
101     g_free(g->name);
102     g_hash_table_destroy(g->key_hash);
103     g_slice_free(ObtDDParseGroup, g);
104 }
105
106 /*! Reads an input string, strips out invalid stuff, and parses
107     backslash-stuff.
108     If @nstrings is not NULL, then it splits the output string at ';'
109     characters.  They are all returned in the same string with null zeros
110     between them, @nstrings is set to the number of such strings.
111  */
112 static gchar* parse_value_string(const gchar *in,
113                                  gboolean locale,
114                                  gulong *nstrings,
115                                  const ObtDDParse *const parse,
116                                  gboolean *error)
117 {
118     const gint bytes = strlen(in);
119     gboolean backslash;
120     gchar *out, *o;
121     const gchar *end, *i;
122
123     g_return_val_if_fail(in != NULL, NULL);
124
125     if (!locale) {
126         end = in + bytes;
127         for (i = in; i < end; ++i) {
128             if ((guchar)*i >= 127 || (guchar)*i < 32) {
129                 /* non-control character ascii */
130                 end = i;
131                 parse_error("Invalid bytes in string", parse, error);
132                 break;
133             }
134         }
135     }
136     else if (!g_utf8_validate(in, bytes, &end))
137         parse_error("Invalid bytes in localestring", parse, error);
138
139     if (nstrings) *nstrings = 1;
140
141     out = g_new(char, bytes + 1);
142     i = in; o = out;
143     backslash = FALSE;
144     while (i < end) {
145         const gchar *next = locale ? g_utf8_find_next_char(i, end) : i+1;
146         if (backslash) {
147             switch(*i) {
148             case 's': *o++ = ' '; break;
149             case 'n': *o++ = '\n'; break;
150             case 't': *o++ = '\t'; break;
151             case 'r': *o++ = '\r'; break;
152             case ';': *o++ = ';'; break;
153             case '\\': *o++ = '\\'; break;
154             default:
155                 parse_error((locale ?
156                              "Invalid escape sequence in localestring" :
157                              "Invalid escape sequence in string"),
158                             parse, error);
159             }
160             backslash = FALSE;
161         }
162         else if (*i == '\\')
163             backslash = TRUE;
164         else if (*i == ';' && nstrings) {
165             ++nstrings;
166             *o = '\0';
167         }
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_value_boolean(const gchar *in,
184                                     const ObtDDParse *const parse,
185                                     gboolean *error)
186 {
187     if (strcmp(in, "true") == 0)
188         return TRUE;
189     else if (strcmp(in, "false") != 0)
190         parse_error("Invalid boolean value", parse, error);
191     return FALSE;
192 }
193
194 static gfloat parse_value_numeric(const gchar *in,
195                                   const ObtDDParse *const parse,
196                                   gboolean *error)
197 {
198     gfloat out = 0;
199     if (sscanf(in, "%f", &out) == 0)
200         parse_error("Invalid numeric value", parse, error);
201     return out;
202 }
203
204 static gboolean parse_file_line(FILE *f, gchar **buf,
205                                 gulong *size, gulong *read,
206                                 ObtDDParse *parse, gboolean *error)
207 {
208     const gulong BUFMUL = 80;
209     size_t ret;
210     gulong i, null;
211
212     if (*size == 0) {
213         g_assert(*read == 0);
214         *size = BUFMUL;
215         *buf = g_new(char, *size);
216     }
217
218     /* remove everything up to a null zero already in the buffer and shift
219        the rest to the front */
220     null = *size;
221     for (i = 0; i < *read; ++i) {
222         if (null < *size)
223             (*buf)[i-null-1] = (*buf)[i];
224         else if ((*buf)[i] == '\0')
225             null = i;
226     }
227     if (null < *size)
228         *read -= null + 1;
229
230     /* is there already a newline in the buffer? */
231     for (i = 0; i < *read; ++i)
232         if ((*buf)[i] == '\n') {
233             /* turn it into a null zero and done */
234             (*buf)[i] = '\0';
235             return TRUE;
236         }
237
238     /* we need to read some more to find a newline */
239     while (TRUE) {
240         gulong eol;
241         gchar *newread;
242
243         newread = *buf + *read;
244         ret = fread(newread, sizeof(char), *size-*read, f);
245         if (ret < *size - *read && !feof(f)) {
246             parse_error("Error reading", parse, error);
247             return FALSE;
248         }
249         *read += ret;
250
251         /* strip out null zeros in the input and look for an endofline */
252         null = 0;
253         eol = *size;
254         for (i = newread-*buf; i < *read; ++i) {
255             if (null > 0)
256                 (*buf)[i] = (*buf)[i+null];
257             if ((*buf)[i] == '\0') {
258                 ++null;
259                 --(*read);
260                 --i; /* try again */
261             }
262             else if ((*buf)[i] == '\n' && eol == *size) {
263                 eol = i;
264                 /* turn it into a null zero */
265                 (*buf)[i] = '\0';
266             }
267         }
268
269         if (eol != *size)
270             /* found an endofline, done */
271             break;
272         else if (feof(f) && *read < *size) {
273             /* found the endoffile, done (if there is space) */
274             if (*read > 0) {
275                 /* stick a null zero on if there is test on the last line */
276                 (*buf)[(*read)++] = '\0';
277             }
278             break;
279         }
280         else {
281             /* read more */
282             size += BUFMUL;
283             *buf = g_renew(char, *buf, *size);
284         }
285     }
286     return *read > 0;
287 }
288
289 static void parse_group(const gchar *buf, gulong len,
290                         ObtDDParse *parse, gboolean *error)
291 {
292     ObtDDParseGroup *g;
293     gchar *group;
294     gulong i;
295
296     /* get the group name */
297     group = g_strndup(buf+1, len-2);
298     for (i = 0; i < len-2; ++i)
299         if ((guchar)group[i] < 32 || (guchar)group[i] >= 127) {
300             /* valid ASCII only */
301             parse_error("Invalid character found", parse, NULL);
302             group[i] = '\0'; /* stopping before this character */
303             break;
304         }
305
306     /* make sure it's a new group */
307     g = g_hash_table_lookup(parse->group_hash, group);
308     if (g && g->seen) {
309         parse_error("Duplicate group found", parse, error);
310         g_free(group);
311         return;
312     }
313     /* if it's the first group, make sure it's named Desktop Entry */
314     else if (!parse->group && strcmp(group, "Desktop Entry") != 0)
315     {
316         parse_error("Incorrect group found, "
317                     "expected [Desktop Entry]",
318                     parse, error);
319         g_free(group);
320         return;
321     }
322     else {
323         if (!g) {
324             g = parse_group_new(group, NULL);
325             g_hash_table_insert(parse->group_hash, g->name, g);
326         }
327         else
328             g_free(group);
329
330         g->seen = TRUE;
331         parse->group = g;
332         g_print("Found group %s\n", g->name);
333     }
334 }
335
336 static void parse_key_value(const gchar *buf, gulong len,
337                             ObtDDParse *parse, gboolean *error)
338 {
339     gulong i, keyend, valstart, eq;
340     char *key;
341
342     /* find the end of the key */
343     for (i = 0; i < len; ++i)
344         if (!(((guchar)buf[i] >= 'A' && (guchar)buf[i] <= 'Z') ||
345               ((guchar)buf[i] >= 'a' && (guchar)buf[i] <= 'z') ||
346               ((guchar)buf[i] >= '0' && (guchar)buf[i] <= '9') ||
347               ((guchar)buf[i] == '-'))) {
348             /* not part of the key */
349             keyend = i;
350             break;
351         }
352     if (keyend < 1) {
353         parse_error("Empty key", parse, error);
354         return;
355     }
356     /* find the = character */
357     for (i = keyend; i < len; ++i) {
358         if (buf[i] == '=') {
359             eq = i;
360             break;
361         }
362         else if (buf[i] != ' ') {
363             parse_error("Invalid character in key name", parse, error);
364             return ;
365         }
366     }
367     if (i == len) {
368         parse_error("Key without value found", parse, error);
369         return;
370     }
371     /* find the start of the value */
372     for (i = eq+1; i < len; ++i)
373         if (buf[i] != ' ') {
374             valstart = i;
375             break;
376         }
377     if (i == len) {
378         parse_error("Empty value found", parse, error);
379         return;
380     }
381
382     key = g_strndup(buf, keyend);
383     if (g_hash_table_lookup(parse->group->key_hash, key)) {
384         parse_error("Duplicate key found", parse, error);
385         g_free(key);
386         return;
387     }
388     g_print("Found key/value %s=%s.\n", key, buf+valstart);
389     if (parse->group->value_func)
390         parse->group->value_func(key, buf+valstart, parse, error);
391 }
392
393 static gboolean parse_file(FILE *f, ObtDDParse *parse)
394 {
395     gchar *buf = NULL;
396     gulong bytes = 0, read = 0;
397     gboolean error = FALSE;
398
399     while (!error && parse_file_line(f, &buf, &bytes, &read, parse, &error)) {
400         gulong len = strlen(buf);
401         if (buf[0] == '#' || buf[0] == '\0')
402             ; /* ignore comment lines */
403         else if (buf[0] == '[' && buf[len-1] == ']')
404             parse_group(buf, len, parse, &error);
405         else if (!parse->group)
406             /* just ignore keys outside of groups */
407             parse_error("Key found before group", parse, NULL);
408         else
409             /* ignore errors in key-value pairs and continue */
410             parse_key_value(buf, len, parse, NULL);
411         ++parse->lineno;
412     }
413
414     if (buf) g_free(buf);
415     return !error;
416 }
417
418 static void parse_desktop_entry_value(gchar *key, const gchar *val,
419                                       ObtDDParse *parse, gboolean *error)
420 {
421     ObtDDParseValue v, *pv;
422
423     /* figure out value type */
424     v.type = OBT_DDPARSE_NUM_VALUE_TYPES;
425     /* XXX do this part */
426
427     /* parse the value */
428     switch (v.type) {
429     case OBT_DDPARSE_STRING:
430         v.value.string = parse_value_string(val, FALSE, NULL, parse, error);
431         g_assert(v.value.string);
432         break;
433     case OBT_DDPARSE_LOCALESTRING:
434         v.value.string = parse_value_string(val, TRUE, NULL, parse, error);
435         g_assert(v.value.string);
436         break;
437     case OBT_DDPARSE_STRINGS:
438         v.value.strings.s = parse_value_string(val, FALSE, &v.value.strings.n,
439                                                parse, error);
440         g_assert(v.value.strings.s);
441         g_assert(v.value.strings.n);
442         break;
443     case OBT_DDPARSE_LOCALESTRINGS:
444         v.value.strings.s = parse_value_string(val, TRUE, &v.value.strings.n,
445                                                parse, error);
446         g_assert(v.value.strings.s);
447         g_assert(v.value.strings.n);
448         break;
449     case OBT_DDPARSE_BOOLEAN:
450         v.value.boolean = parse_value_boolean(val, parse, error);
451         break;
452     case OBT_DDPARSE_NUMERIC:
453         v.value.numeric = parse_value_numeric(val, parse, error);
454         break;
455     default:
456         g_assert_not_reached();
457     }
458
459     pv = g_slice_new(ObtDDParseValue);
460     *pv = v;
461     g_hash_table_insert(parse->group->key_hash, key, pv);
462 }
463
464 GHashTable* obt_ddparse_file(const gchar *name, GSList *paths)
465 {
466     ObtDDParse parse;
467     ObtDDParseGroup *desktop_entry;
468     GSList *it;
469     FILE *f;
470     gboolean success;
471
472     parse.filename = NULL;
473     parse.lineno = 0;
474     parse.group = NULL;
475     parse.group_hash = g_hash_table_new_full(g_str_hash,
476                                              g_str_equal,
477                                              NULL,
478                                              (GDestroyNotify)parse_group_free);
479
480     /* set up the groups (there's only one right now) */
481     desktop_entry = parse_group_new(g_strdup("Desktop Entry"),
482                                     parse_desktop_entry_value);
483     g_hash_table_insert(parse.group_hash, desktop_entry->name, desktop_entry);
484
485     success = FALSE;
486     for (it = paths; it && !success; it = g_slist_next(it)) {
487         gchar *path = g_strdup_printf("%s/%s", (char*)it->data, name);
488         if ((f = fopen(path, "r"))) {
489             parse.filename = path;
490             parse.lineno = 1;
491             success = parse_file(f, &parse);
492             fclose(f);
493         }
494         g_free(path);
495     }
496     if (!success) {
497         g_hash_table_destroy(parse.group_hash);
498         return NULL;
499     }
500     else
501         return parse.group_hash;
502 }
503
504 GHashTable* obt_ddparse_group_keys(ObtDDParseGroup *g)
505 {
506     return g->key_hash;
507 }