2007-04-24 Jonathan Chambers <joncham@gmail.com>
[mono.git] / eglib / src / gmarkup.c
1 /*
2  * gmakrup.c: Minimal XML markup reader.
3  *
4  * Unlike the GLib one, this can not be restarted with more text
5  * as the Mono use does not require it.
6  *
7  * Actually, with further thought, I think that this could be made
8  * to restart very easily.  The pos == end condition would mean
9  * "return to caller" and only at end parse this would be a fatal
10  * error.
11  *
12  * Not that it matters to Mono, but it is very simple to change, there
13  * is a tricky situation: there are a few places where we check p+n
14  * in the source, and that would have to change to be progressive, instead
15  * of depending on the string to be complete at that point, so we would
16  * have to introduce extra states to cope with that.
17  *
18  * Author:
19  *   Miguel de Icaza (miguel@novell.com)
20  *
21  * (C) 2006 Novell, Inc.
22  *
23  * Permission is hereby granted, free of charge, to any person obtaining
24  * a copy of this software and associated documentation files (the
25  * "Software"), to deal in the Software without restriction, including
26  * without limitation the rights to use, copy, modify, merge, publish,
27  * distribute, sublicense, and/or sell copies of the Software, and to
28  * permit persons to whom the Software is furnished to do so, subject to
29  * the following conditions:
30  *
31  * The above copyright notice and this permission notice shall be
32  * included in all copies or substantial portions of the Software.
33  *
34  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
35  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
36  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
37  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
38  * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
39  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41  */
42 #include <stdio.h>
43 #include <glib.h>
44
45 #define set_error(msg, ...) do { if (error != NULL) *error = g_error_new (GINT_TO_POINTER (1), 1, msg, __VA_ARGS__); } while (0);
46
47 typedef enum {
48         START,
49         START_ELEMENT,
50         TEXT,
51         FLUSH_TEXT,
52         CLOSING_ELEMENT,
53         COMMENT,
54         SKIP_XML_DECLARATION
55 } ParseState;
56
57 struct _GMarkupParseContext {
58         GMarkupParser  parser;
59         gpointer       user_data;
60         GDestroyNotify user_data_dnotify;
61         ParseState     state;
62
63         /* Stores the name of the current element, so we can issue the end_element */
64         GSList         *level;
65
66         GString        *text;
67 };
68
69 GMarkupParseContext *
70 g_markup_parse_context_new (const GMarkupParser *parser,
71                             GMarkupParseFlags flags,
72                             gpointer user_data,
73                             GDestroyNotify user_data_dnotify)
74 {
75         GMarkupParseContext *context = g_new0 (GMarkupParseContext, 1);
76
77         context->parser = *parser;
78         context->user_data = user_data;
79         context->user_data_dnotify = user_data_dnotify;
80
81         return context;
82 }
83
84 void
85 g_markup_parse_context_free (GMarkupParseContext *context)
86 {
87         GSList *l;
88         
89         g_return_if_fail (context != NULL);
90
91         if (context->user_data_dnotify != NULL)
92                 (context->user_data_dnotify) (context->user_data);
93         
94         if (context->text != NULL)
95                 g_string_free (context->text, TRUE);
96         for (l = context->level; l; l = l->next)
97                 g_free (l->data);
98         g_slist_free (context->level);
99         g_free (context);
100 }
101
102 static const char *
103 skip_space (const char *p, const char *end)
104 {
105         for (; p < end && isspace (*p); p++)
106                 ;
107         return p;
108 }
109
110 static const char *
111 parse_value (const char *p, const char *end, char **value, GError **error)
112 {
113         const char *start;
114         int l;
115         
116         if (*p != '"'){
117                 set_error ("%s", "Expected the attribute value to start with a quote");
118                 return end;
119         }
120         start = ++p;
121         for (; p < end && *p != '"'; p++)
122                 ;
123         if (p == end)
124                 return end;
125         l = p - start;
126         p++;
127         *value = malloc (l + 1);
128         if (*value == NULL)
129                 return end;
130         strncpy (*value, start, l);
131         (*value) [l] = 0;
132         return p;
133 }
134
135 static const char *
136 parse_name (const char *p, const char *end, char **value)
137 {
138         const char *start = p;
139         int l;
140         
141         for (; p < end && isalnum (*p); p++)
142                 ;
143         if (p == end)
144                 return end;
145
146         l = p - start;
147         *value = malloc (l + 1);
148         if (*value == NULL)
149                 return end;
150         strncpy (*value, start, l);
151         (*value) [l] = 0;
152         return p;
153 }
154
155 static const char *
156 parse_attributes (const char *p, const char *end, char ***names, char ***values, GError **error, int *full_stop, int state)
157 {
158         int nnames = 0;
159
160         while (TRUE){
161                 p = skip_space (p, end);
162                 if (p == end)
163                         return end;
164                         
165                 if (*p == '>'){
166                         *full_stop = 0;
167                         return p; 
168                 }
169                 if (state == SKIP_XML_DECLARATION && *p == '?' && ((p+1) < end) && *(p+1) == '>'){
170                         *full_stop = 0;
171                         return p+1;
172                 }
173                 
174                 if (*p == '/' && ((p+1) < end && *(p+1) == '>')){
175                         *full_stop = 1;
176                         return p+1;
177                 } else {
178                         char *name, *value;
179                         
180                         p = parse_name (p, end, &name);
181                         if (p == end)
182                                 return p;
183
184                         p = skip_space (p, end);
185                         if (p == end){
186                                 free (name);
187                                 return p;
188                         }
189                         if (*p != '='){
190                                 set_error ("Expected an = after the attribute name `%s'", name);
191                                 free (name);
192                                 return end;
193                         }
194                         p++;
195                         p = skip_space (p, end);
196                         if (p == end){
197                                 free (name);
198                                 return end;
199                         }
200
201                         p = parse_value (p, end, &value, error);
202                         if (p == end){
203                                 free (name);
204                                 return p;
205                         }
206
207                         ++nnames;
208                         *names = g_realloc (*names, sizeof (char **) * (nnames+1));
209                         *values = g_realloc (*values, sizeof (char **) * (nnames+1));
210                         (*names) [nnames-1] = name;
211                         (*values) [nnames-1] = value;
212                         (*names) [nnames] = NULL;
213                         (*values) [nnames] = NULL;                      
214                 }
215         } 
216 }
217
218 static void
219 destroy_parse_state (GMarkupParseContext *context)
220 {
221         GSList *p;
222
223         for (p = context->level; p != NULL; p = p->next)
224                 g_free (p->data);
225         
226         g_slist_free (context->level);
227         if (context->text != NULL)
228                 g_string_free (context->text, TRUE);
229         context->text = NULL;
230         context->level = NULL;
231 }
232
233 gboolean
234 g_markup_parse_context_parse (GMarkupParseContext *context,
235                               const gchar *text, gssize text_len,
236                               GError **error)
237 {
238         const char *p,  *end;
239         
240         g_return_val_if_fail (context != NULL, FALSE);
241         g_return_val_if_fail (text != NULL, FALSE);
242         g_return_val_if_fail (text_len >= 0, FALSE);
243
244         end = text + text_len;
245         
246         for (p = text; p < end; p++){
247                 char c = *p;
248
249                 switch (context->state){
250                 case START:
251                         if (c == ' ' || c == '\t' || c == '\f' || c == '\n')
252                                 continue;
253                         if (c == '<'){
254                                 if (p+1 < end && p [1] == '?'){
255                                         context->state = SKIP_XML_DECLARATION;
256                                         p++;
257                                 } else
258                                         context->state = START_ELEMENT;
259                                 continue;
260                         }
261                         set_error ("%s", "Expected < to start the document");
262                         goto fail;
263
264                 case SKIP_XML_DECLARATION:
265                 case START_ELEMENT: {
266                         const char *element_start = p, *element_end;
267                         char *ename = NULL;
268                         int full_stop = 0, l;
269                         gchar **names = NULL, **values = NULL;
270
271                         for (; p < end && isspace (*p); p++)
272                                 ;
273                         if (p == end){
274                                 set_error ("%s", "Unfinished element");
275                                 goto fail;
276                         }
277
278                         if (*p == '!' && (p+2 < end) && (p [1] == '-') && (p [2] == '-')){
279                                 context->state = COMMENT;
280                                 p += 2;
281                                 break;
282                         }
283                         
284                         if (!(isascii (*p) && isalpha (*p))){
285                                 set_error ("%s", "Expected an element name");
286                                 goto fail;
287                         }
288                         
289                         for (++p; p < end && (isalnum (*p) || (*p == '.')); p++)
290                                 ;
291                         if (p == end){
292                                 set_error ("%s", "Expected an element");
293                                 goto fail;
294                         }
295                         element_end = p;
296                         
297                         for (; p < end && isspace (*p); p++)
298                                 ;
299                         if (p == end){
300                                 set_error ("%s", "Unfinished element");
301                                 goto fail;
302                         }
303                         p = parse_attributes (p, end, &names, &values, error, &full_stop, context->state);
304                         if (p == end){
305                                 if (names != NULL) {
306                                         g_strfreev (names);
307                                         g_strfreev (values);
308                                 }
309                                 /* Only set the error if parse_attributes did not */
310                                 if (error != NULL && *error == NULL)
311                                         set_error ("%s", "Unfinished sequence");
312                                 goto fail;
313                         }
314                         l = element_end - element_start;
315                         ename = malloc (l + 1);
316                         if (ename == NULL)
317                                 goto fail;
318                         strncpy (ename, element_start, l);
319                         ename [l] = 0;
320
321                         if (context->state == START_ELEMENT)
322                                 if (context->parser.start_element != NULL)
323                                         context->parser.start_element (context, ename,
324                                                                        (const gchar **) names,
325                                                                        (const gchar **) values,
326                                                                        context->user_data, error);
327
328                         if (names != NULL){
329                                 g_strfreev (names);
330                                 g_strfreev (values);
331                         }
332
333                         if (error != NULL && *error != NULL){
334                                 free (ename);
335                                 goto fail;
336                         }
337                         
338                         if (full_stop){
339                                 if (context->parser.end_element != NULL &&  context->state == START_ELEMENT){
340                                         context->parser.end_element (context, ename, context->user_data, error);
341                                         if (error != NULL && *error != NULL){
342                                                 free (ename);
343                                                 goto fail;
344                                         }
345                                 }
346                                 free (ename);
347                         } else {
348                                 context->level = g_slist_prepend (context->level, ename);
349                         }
350                         
351                         context->state = TEXT;
352                         break;
353                 } /* case START_ELEMENT */
354
355                 case TEXT: {
356                         if (c == '<'){
357                                 context->state = FLUSH_TEXT;
358                                 break;
359                         }
360                         if (context->parser.text != NULL){
361                                 if (context->text == NULL)
362                                         context->text = g_string_new ("");
363                                 g_string_append_c (context->text, c);
364                         }
365                         break;
366                 }
367
368                 case COMMENT:
369                         if (*p != '-')
370                                 break;
371                         if (p+2 < end && (p [1] == '-') && (p [2] == '>')){
372                                 context->state = TEXT;
373                                 p += 2;
374                                 break;
375                         }
376                         break;
377                         
378                 case FLUSH_TEXT:
379                         if (context->parser.text != NULL){
380                                 context->parser.text (context, context->text->str, context->text->len,
381                                                       context->user_data, error);
382                                 if (error != NULL && *error != NULL)
383                                         goto fail;
384                         }
385                         
386                         if (c == '/')
387                                 context->state = CLOSING_ELEMENT;
388                         else {
389                                 p--;
390                                 context->state = START_ELEMENT;
391                         }
392                         break;
393
394                 case CLOSING_ELEMENT: {
395                         GSList *current = context->level;
396                         char *text;
397
398                         if (context->level == NULL){
399                                 set_error ("%s", "Too many closing tags, not enough open tags");
400                                 goto fail;
401                         }
402                         
403                         text = current->data;
404                         if (context->parser.end_element != NULL){
405                                 context->parser.end_element (context, text, context->user_data, error);
406                                 if (error != NULL && *error != NULL){
407                                         free (text);
408                                         goto fail;
409                                 }
410                         }
411                         free (text);
412
413                         while (p < end && *p != '>')
414                                 p++;
415                         
416                         context->level = context->level->next;
417                         g_slist_free_1 (current);
418                         context->state = TEXT;
419                         break;
420                 } /* case CLOSING_ELEMENT */
421                         
422                 } /* switch */
423         }
424
425
426         return TRUE;
427  fail:
428         if (context->parser.error && error != NULL && *error)
429                 context->parser.error (context, *error, context->user_data);
430         
431         destroy_parse_state (context);
432         return FALSE;
433 }
434
435 gboolean
436 g_markup_parse_context_end_parse (GMarkupParseContext *context, GError **error)
437 {
438         g_return_val_if_fail (context != NULL, FALSE);
439
440         /*
441          * In our case, we always signal errors during parse, not at the end
442          * see the notes at the top of this file for details on how this
443          * could be moved here
444          */
445         return TRUE;
446 }