inital gtk2 patch
[claws.git] / src / common / xml.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2001 Hiroyuki Yamamoto
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 #include <glib.h>
21 #include <stdio.h>
22 #include <string.h>
23 #include <ctype.h>
24
25 #include "xml.h"
26 #include "utils.h"
27 #include "../codeconv.h"
28
29 #define SPARSE_MEMORY
30 /* if this is defined all attr.names and tag.names are stored
31  * in a hash table */
32 #if defined(SPARSE_MEMORY)
33 #include "stringtable.h" 
34
35 static StringTable *xml_string_table;
36
37 static void xml_string_table_create(void)
38 {
39         if (xml_string_table == NULL)
40                 xml_string_table = string_table_new();
41 }
42 #define XML_STRING_ADD(str) \
43         string_table_insert_string(xml_string_table, (str))
44 #define XML_STRING_FREE(str) \
45         string_table_free_string(xml_string_table, (str))
46
47 #define XML_STRING_TABLE_CREATE() \
48         xml_string_table_create()
49
50 #else /* !SPARSE_MEMORY */
51
52 #define XML_STRING_ADD(str) \
53         g_strdup(str)
54 #define XML_STRING_FREE(str) \
55         g_free(str)
56
57 #define XML_STRING_TABLE_CREATE()
58
59 #endif /* SPARSE_MEMORY */
60
61 static void xml_free_tag        (XMLTag         *tag);
62 static gint xml_get_parenthesis (XMLFile        *file,
63                                  gchar          *buf,
64                                  gint            len);
65
66 XMLFile *xml_open_file(const gchar *path)
67 {
68         XMLFile *newfile;
69
70         g_return_val_if_fail(path != NULL, NULL);
71
72         XML_STRING_TABLE_CREATE();
73
74         newfile = g_new(XMLFile, 1);
75
76         newfile->fp = fopen(path, "rb");
77         if (!newfile->fp) {
78                 g_free(newfile);
79                 return NULL;
80         }
81
82         newfile->buf = g_string_new(NULL);
83         newfile->bufp = newfile->buf->str;
84
85         newfile->dtd = NULL;
86         newfile->tag_stack = NULL;
87         newfile->level = 0;
88         newfile->is_empty_element = FALSE;
89
90         return newfile;
91 }
92
93 void xml_close_file(XMLFile *file)
94 {
95         g_return_if_fail(file != NULL);
96
97         if (file->fp) fclose(file->fp);
98
99         g_string_free(file->buf, TRUE);
100
101         g_free(file->dtd);
102
103         while (file->tag_stack != NULL)
104                 xml_pop_tag(file);
105
106         g_free(file);
107 }
108
109 static GNode *xml_build_tree(XMLFile *file, GNode *parent, guint level)
110 {
111         GNode *node = NULL;
112         XMLNode *xmlnode;
113         XMLTag *tag;
114
115         while (xml_parse_next_tag(file) == 0) {
116                 if (file->level < level) break;
117                 if (file->level == level) {
118                         g_warning("xml_build_tree(): Parse error\n");
119                         break;
120                 }
121
122                 tag = xml_get_current_tag(file);
123                 if (!tag) break;
124                 xmlnode = g_new(XMLNode, 1);
125                 xmlnode->tag = xml_copy_tag(tag);
126                 xmlnode->element = xml_get_element(file);
127                 if (!parent)
128                         node = g_node_new(xmlnode);
129                 else
130                         node = g_node_append_data(parent, xmlnode);
131
132                 xml_build_tree(file, node, file->level);
133                 if (file->level == 0) break;
134         }
135
136         return node;
137 }
138
139 GNode *xml_parse_file(const gchar *path)
140 {
141         XMLFile *file;
142         GNode *node;
143
144         file = xml_open_file(path);
145         g_return_val_if_fail(file != NULL, NULL);
146
147         xml_get_dtd(file);
148
149         node = xml_build_tree(file, NULL, file->level);
150
151         xml_close_file(file);
152
153 #if defined(SPARSE_MEMORY)
154         if (debug_get_mode())
155                 string_table_get_stats(xml_string_table);
156 #endif
157
158         return node;
159 }
160
161 gint xml_get_dtd(XMLFile *file)
162 {
163         gchar buf[XMLBUFSIZE];
164         gchar *bufp = buf;
165
166         if (xml_get_parenthesis(file, buf, sizeof(buf)) < 0) return -1;
167
168         if ((*bufp++ == '?') &&
169             (bufp = strcasestr(bufp, "xml")) &&
170             (bufp = strcasestr(bufp + 3, "version")) &&
171             (bufp = strchr(bufp + 7, '?')))
172                 file->dtd = g_strdup(buf);
173         else {
174                 g_warning("Can't get xml dtd\n");
175                 return -1;
176         }
177
178         return 0;
179 }
180
181 gint xml_parse_next_tag(XMLFile *file)
182 {
183         gchar buf[XMLBUFSIZE];
184         gchar *bufp = buf;
185         XMLTag *tag;
186         gint len;
187
188         if (file->is_empty_element == TRUE) {
189                 file->is_empty_element = FALSE;
190                 xml_pop_tag(file);
191                 return 0;
192         }
193
194         if (xml_get_parenthesis(file, buf, sizeof(buf)) < 0) {
195                 g_warning("xml_parse_next_tag(): Can't parse next tag\n");
196                 return -1;
197         }
198
199         /* end-tag */
200         if (buf[0] == '/') {
201                 if (strcmp(xml_get_current_tag(file)->tag, buf + 1) != 0) {
202                         g_warning("xml_parse_next_tag(): Tag name mismatch: %s\n", buf);
203                         return -1;
204                 }
205                 xml_pop_tag(file);
206                 return 0;
207         }
208
209         tag = g_new0(XMLTag, 1);
210         xml_push_tag(file, tag);
211
212         len = strlen(buf);
213         if (len > 0 && buf[len - 1] == '/') {
214                 file->is_empty_element = TRUE;
215                 buf[len - 1] = '\0';
216                 g_strchomp(buf);
217         }
218         if (strlen(buf) == 0) {
219                 g_warning("xml_parse_next_tag(): Tag name is empty\n");
220                 return -1;
221         }
222
223         while (*bufp != '\0' && !isspace(*bufp)) bufp++;
224         if (*bufp == '\0') {
225                 tag->tag = XML_STRING_ADD(buf);
226                 return 0;
227         } else {
228                 *bufp++ = '\0';
229                 tag->tag = XML_STRING_ADD(buf);
230         }
231
232         /* parse attributes ( name=value ) */
233         while (*bufp) {
234                 XMLAttr *attr;
235                 gchar *attr_name, *attr_value;
236                 gchar *utf8attr_name, *utf8attr_value;
237                 gchar *p;
238                 gchar quote;
239
240                 while (isspace(*bufp)) bufp++;
241                 attr_name = bufp;
242                 if ((p = strchr(attr_name, '=')) == NULL) {
243                         g_warning("xml_parse_next_tag(): Syntax error in tag\n");
244                         return -1;
245                 }
246                 bufp = p;
247                 *bufp++ = '\0';
248                 while (isspace(*bufp)) bufp++;
249
250                 if (*bufp != '"' && *bufp != '\'') {
251                         g_warning("xml_parse_next_tag(): Syntax error in tag\n");
252                         return -1;
253                 }
254                 quote = *bufp;
255                 bufp++;
256                 attr_value = bufp;
257                 if ((p = strchr(attr_value, quote)) == NULL) {
258                         g_warning("xml_parse_next_tag(): Syntax error in tag\n");
259                         return -1;
260                 }
261                 bufp = p;
262                 *bufp++ = '\0';
263
264                 g_strchomp(attr_name);
265                 xml_unescape_str(attr_value);
266
267                 attr = g_new(XMLAttr, 1);
268 #warning FIXME_GTK2
269                 utf8attr_name  = conv_codeset_strdup
270                                         (attr_name,
271                                          conv_get_current_charset_str(),
272                                          CS_UTF_8);
273                 utf8attr_value = conv_codeset_strdup
274                                         (attr_value,
275                                          conv_get_current_charset_str(),
276                                          CS_UTF_8);
277                 if (!utf8attr_name) {
278                         g_warning("xml_parse_next_tag(): "
279                                   "faild to convert character set of attr_name\n");
280                         utf8attr_name = g_strdup(attr_name);
281                 }
282                 if (!utf8attr_value) {
283                         g_warning("xml_parse_next_tag(): "
284                                   "faild to convert character set of attr_value\n");
285                         utf8attr_value = g_strdup(attr_value);
286                 }
287
288                 attr->name  = XML_STRING_ADD(utf8attr_name);
289                 attr->value = utf8attr_value;
290                 tag->attr   = g_list_append(tag->attr, attr);
291
292                 g_free(utf8attr_name);
293         }
294
295         return 0;
296 }
297
298 void xml_push_tag(XMLFile *file, XMLTag *tag)
299 {
300         g_return_if_fail(tag != NULL);
301
302         file->tag_stack = g_list_prepend(file->tag_stack, tag);
303         file->level++;
304 }
305
306 void xml_pop_tag(XMLFile *file)
307 {
308         XMLTag *tag;
309
310         if (!file->tag_stack) return;
311
312         tag = (XMLTag *)file->tag_stack->data;
313
314         xml_free_tag(tag);
315         file->tag_stack = g_list_remove(file->tag_stack, tag);
316         file->level--;
317 }
318
319 XMLTag *xml_get_current_tag(XMLFile *file)
320 {
321         if (file->tag_stack)
322                 return (XMLTag *)file->tag_stack->data;
323         else
324                 return NULL;
325 }
326
327 GList *xml_get_current_tag_attr(XMLFile *file)
328 {
329         XMLTag *tag;
330
331         tag = xml_get_current_tag(file);
332         if (!tag) return NULL;
333
334         return tag->attr;
335 }
336
337 gchar *xml_get_element(XMLFile *file)
338 {
339         gchar *str;
340         gchar *end;
341         gchar *utf8str;
342
343         while ((end = strchr(file->bufp, '<')) == NULL)
344                 if (xml_read_line(file) < 0) return NULL;
345
346         if (end == file->bufp)
347                 return NULL;
348
349         str = g_strndup(file->bufp, end - file->bufp);
350         /* this is not XML1.0 strict */
351         g_strstrip(str);
352         xml_unescape_str(str);
353
354         file->bufp = end;
355         xml_truncate_buf(file);
356
357         if (str[0] == '\0') {
358                 g_free(str);
359                 return NULL;
360         }
361
362         utf8str = conv_codeset_strdup
363                         (str,
364                          conv_get_current_charset_str(),
365                          CS_UTF_8);
366         if (!utf8str) {
367                 g_warning("xml_get_element(): "
368                           "faild to convert character set.\n");
369                 utf8str = str;
370         } else
371                 g_free(str);
372
373         return utf8str;
374 }
375
376 gint xml_read_line(XMLFile *file)
377 {
378         gchar buf[XMLBUFSIZE];
379         gint index;
380
381         if (fgets(buf, sizeof(buf), file->fp) == NULL)
382                 return -1;
383
384         index = file->bufp - file->buf->str;
385
386         g_string_append(file->buf, buf);
387
388         file->bufp = file->buf->str + index;
389
390         return 0;
391 }
392
393 void xml_truncate_buf(XMLFile *file)
394 {
395         gint len;
396
397         len = file->bufp - file->buf->str;
398         if (len > 0) {
399                 g_string_erase(file->buf, 0, len);
400                 file->bufp = file->buf->str;
401         }
402 }
403
404 gboolean xml_compare_tag(XMLFile *file, const gchar *name)
405 {
406         XMLTag *tag;
407
408         tag = xml_get_current_tag(file);
409
410         if (tag && strcmp(tag->tag, name) == 0)
411                 return TRUE;
412         else
413                 return FALSE;
414 }
415
416 XMLTag *xml_copy_tag(XMLTag *tag)
417 {
418         XMLTag *new_tag;
419         XMLAttr *attr;
420         GList *list;
421
422         new_tag = g_new(XMLTag, 1);
423         new_tag->tag = XML_STRING_ADD(tag->tag);
424         new_tag->attr = NULL;
425         for (list = tag->attr; list != NULL; list = list->next) {
426                 attr = xml_copy_attr((XMLAttr *)list->data);
427                 new_tag->attr = g_list_append(new_tag->attr, attr);
428         }
429
430         return new_tag;
431 }
432
433 XMLAttr *xml_copy_attr(XMLAttr *attr)
434 {
435         XMLAttr *new_attr;
436
437         new_attr = g_new(XMLAttr, 1);
438         new_attr->name  = XML_STRING_ADD(attr->name);
439         new_attr->value = g_strdup(attr->value);
440
441         return new_attr;
442 }
443
444 gint xml_unescape_str(gchar *str)
445 {
446         gchar *start;
447         gchar *end;
448         gchar *p = str;
449         gchar *esc_str;
450         gchar ch;
451         gint len;
452
453         while ((start = strchr(p, '&')) != NULL) {
454                 if ((end = strchr(start + 1, ';')) == NULL) {
455                         g_warning("Unescaped `&' appeared\n");
456                         p = start + 1;
457                         continue;
458                 }
459                 len = end - start + 1;
460                 if (len < 3) {
461                         p = end + 1;
462                         continue;
463                 }
464
465                 Xstrndup_a(esc_str, start, len, return -1);
466                 if (!strcmp(esc_str, "&lt;"))
467                         ch = '<';
468                 else if (!strcmp(esc_str, "&gt;"))
469                         ch = '>';
470                 else if (!strcmp(esc_str, "&amp;"))
471                         ch = '&';
472                 else if (!strcmp(esc_str, "&apos;"))
473                         ch = '\'';
474                 else if (!strcmp(esc_str, "&quot;"))
475                         ch = '\"';
476                 else {
477                         p = end + 1;
478                         continue;
479                 }
480
481                 *start = ch;
482                 memmove(start + 1, end + 1, strlen(end + 1) + 1);
483                 p = start + 1;
484         }
485
486         return 0;
487 }
488
489 #warning FIXME_GTK2
490 gint xml_file_put_escape_str(FILE *fp, const gchar *str)
491 {
492         const gchar *src_codeset = CS_UTF_8;
493         const gchar *dest_codeset = conv_get_current_charset_str();
494         gchar *tmpstr = NULL;
495         const gchar *p;
496
497         g_return_val_if_fail(fp != NULL, -1);
498
499         if (!str) return 0;
500
501         tmpstr = conv_codeset_strdup(str, src_codeset, dest_codeset);
502         if (!tmpstr) {
503                 g_warning("xml_file_put_escape_str(): Faild to convert character set.");
504                 tmpstr = g_strdup(str);
505         }
506
507         for (p = tmpstr; *p != '\0'; p++) {
508                 switch (*p) {
509                 case '<':
510                         fputs("&lt;", fp);
511                         break;
512                 case '>':
513                         fputs("&gt;", fp);
514                         break;
515                 case '&':
516                         fputs("&amp;", fp);
517                         break;
518                 case '\'':
519                         fputs("&apos;", fp);
520                         break;
521                 case '\"':
522                         fputs("&quot;", fp);
523                         break;
524                 default:
525                         fputc(*p, fp);
526                 }
527         }
528
529         g_free(tmpstr);
530
531         return 0;
532 }
533
534 void xml_free_node(XMLNode *node)
535 {
536         if (!node) return;
537
538         xml_free_tag(node->tag);
539         g_free(node->element);
540         g_free(node);
541 }
542
543 static gboolean xml_free_func(GNode *node, gpointer data)
544 {
545         XMLNode *xmlnode = node->data;
546
547         xml_free_node(xmlnode);
548         return FALSE;
549 }
550
551 void xml_free_tree(GNode *node)
552 {
553         g_return_if_fail(node != NULL);
554
555         g_node_traverse(node, G_PRE_ORDER, G_TRAVERSE_ALL, -1, xml_free_func,
556                         NULL);
557
558         g_node_destroy(node);
559 }
560
561 static void xml_free_tag(XMLTag *tag)
562 {
563         if (!tag) return;
564
565         XML_STRING_FREE(tag->tag);
566         while (tag->attr != NULL) {
567                 XMLAttr *attr = (XMLAttr *)tag->attr->data;
568                 XML_STRING_FREE(attr->name);
569                 g_free(attr->value);
570                 g_free(attr);
571                 tag->attr = g_list_remove(tag->attr, tag->attr->data);
572         }
573         g_free(tag);
574 }
575
576 static gint xml_get_parenthesis(XMLFile *file, gchar *buf, gint len)
577 {
578         gchar *start;
579         gchar *end;
580
581         buf[0] = '\0';
582
583         while ((start = strchr(file->bufp, '<')) == NULL)
584                 if (xml_read_line(file) < 0) return -1;
585
586         start++;
587         file->bufp = start;
588
589         while ((end = strchr(file->bufp, '>')) == NULL)
590                 if (xml_read_line(file) < 0) return -1;
591
592         strncpy2(buf, file->bufp, MIN(end - file->bufp + 1, len));
593         g_strstrip(buf);
594         file->bufp = end + 1;
595         xml_truncate_buf(file);
596
597         return 0;
598 }