]> wimlib.net Git - wimlib/blob - src/xmlproc.c
compiler.h: remove _may_alias_attribute
[wimlib] / src / xmlproc.c
1 /*
2  * xmlproc.c
3  *
4  * A simple XML 1.0 processor.  This handles all XML features that are used in
5  * WIM files, plus a bit more for futureproofing.  It omits problematic
6  * features, such as expansion of entities other than simple escape sequences.
7  */
8
9 /*
10  * Copyright 2023 Eric Biggers
11  *
12  * This file is free software; you can redistribute it and/or modify it under
13  * the terms of the GNU Lesser General Public License as published by the Free
14  * Software Foundation; either version 3 of the License, or (at your option) any
15  * later version.
16  *
17  * This file is distributed in the hope that it will be useful, but WITHOUT
18  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19  * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
20  * details.
21  *
22  * You should have received a copy of the GNU Lesser General Public License
23  * along with this file; if not, see http://www.gnu.org/licenses/.
24  */
25
26 #ifdef HAVE_CONFIG_H
27 #  include "config.h"
28 #endif
29
30 #include <string.h>
31
32 #include "wimlib/error.h"
33 #include "wimlib/test_support.h"
34 #include "wimlib/util.h"
35 #include "wimlib/xmlproc.h"
36
37 /*----------------------------------------------------------------------------*
38  *                         XML node utility functions                         *
39  *----------------------------------------------------------------------------*/
40
41 static tchar *
42 tstrdupz(const tchar *str, size_t len)
43 {
44         tchar *new_str = CALLOC(len + 1, sizeof(str[0]));
45
46         if (new_str)
47                 tmemcpy(new_str, str, len);
48         return new_str;
49 }
50
51 static struct xml_node *
52 xml_new_node(struct xml_node *parent, enum xml_node_type type,
53              const tchar *name, size_t name_len,
54              const tchar *value, size_t value_len)
55 {
56         struct xml_node *node = CALLOC(1, sizeof(*node));
57
58         if (!node)
59                 return NULL;
60         node->type = type;
61         INIT_LIST_HEAD(&node->children);
62         if (name) {
63                 node->name = tstrdupz(name, name_len);
64                 if (!node->name)
65                         goto oom;
66         }
67         if (value) {
68                 node->value = tstrdupz(value, value_len);
69                 if (!node->value)
70                         goto oom;
71         }
72         if (parent)
73                 xml_add_child(parent, node);
74         return node;
75
76 oom:
77         xml_free_node(node);
78         return NULL;
79 }
80
81 /*
82  * Create a new ELEMENT node, and if @parent is non-NULL add the new node under
83  * @parent which should be another ELEMENT.
84  */
85 struct xml_node *
86 xml_new_element(struct xml_node *parent, const tchar *name)
87 {
88         return xml_new_node(parent, XML_ELEMENT_NODE, name, tstrlen(name),
89                             NULL, 0);
90 }
91
92 /*
93  * Create a new ELEMENT node with an attached TEXT node, and if @parent is
94  * non-NULL add the new ELEMENT under @parent which should be another ELEMENT.
95  */
96 struct xml_node *
97 xml_new_element_with_text(struct xml_node *parent, const tchar *name,
98                           const tchar *text)
99 {
100         struct xml_node *element = xml_new_element(parent, name);
101
102         if (element && xml_element_set_text(element, text) != 0) {
103                 xml_free_node(element);
104                 return NULL;
105         }
106         return element;
107 }
108
109 /* Append @child to the children list of @parent. */
110 void
111 xml_add_child(struct xml_node *parent, struct xml_node *child)
112 {
113         xml_unlink_node(child); /* Shouldn't be needed, but be safe. */
114         child->parent = parent;
115         list_add_tail(&child->sibling_link, &parent->children);
116 }
117
118 /* Unlink @node from its parent, if it has one. */
119 void
120 xml_unlink_node(struct xml_node *node)
121 {
122         if (node->parent) {
123                 list_del(&node->sibling_link);
124                 node->parent = NULL;
125         }
126 }
127
128 static void
129 xml_free_children(struct xml_node *parent)
130 {
131         struct xml_node *child, *tmp;
132
133         list_for_each_entry_safe(child, tmp, &parent->children, sibling_link)
134                 xml_free_node(child);
135 }
136
137 /* Recursively free @node, first unlinking it if needed.  @node may be NULL. */
138 void
139 xml_free_node(struct xml_node *node)
140 {
141         if (node) {
142                 xml_unlink_node(node);
143                 xml_free_children(node);
144                 FREE(node->name);
145                 FREE(node->value);
146                 FREE(node);
147         }
148 }
149
150 /*
151  * Return the text from the first TEXT child node of @element, or NULL if no
152  * such node exists.  @element may be NULL.
153  */
154 const tchar *
155 xml_element_get_text(const struct xml_node *element)
156 {
157         const struct xml_node *child;
158
159         xml_node_for_each_child(element, child)
160                 if (child->type == XML_TEXT_NODE)
161                         return child->value;
162         return NULL;
163 }
164
165 /*
166  * Set the contents of the given @element to the given @text, replacing the
167  * entire existing contents if any.
168  */
169 int
170 xml_element_set_text(struct xml_node *element, const tchar *text)
171 {
172         struct xml_node *text_node = xml_new_node(NULL, XML_TEXT_NODE, NULL, 0,
173                                                   text, tstrlen(text));
174         if (!text_node)
175                 return WIMLIB_ERR_NOMEM;
176         xml_free_children(element);
177         xml_add_child(element, text_node);
178         return 0;
179 }
180
181 static int
182 xml_element_append_text(struct xml_node *element,
183                         const tchar *text, size_t text_len)
184 {
185         struct xml_node *last_child;
186
187         if (!list_empty(&element->children) &&
188             (last_child =
189              list_last_entry(&element->children, struct xml_node,
190                              sibling_link))->type == XML_TEXT_NODE) {
191                 /*
192                  * The new TEXT would directly follow another TEXT, so simplify
193                  * the tree by just appending to the existing TEXT.  (This case
194                  * can theoretically be reached via the use of CDATA...)
195                  */
196                 size_t old_len = tstrlen(last_child->value);
197                 tchar *new_value = CALLOC(old_len + text_len + 1,
198                                           sizeof(new_value[0]));
199                 if (!new_value)
200                         return WIMLIB_ERR_NOMEM;
201                 tmemcpy(new_value, last_child->value, old_len);
202                 tmemcpy(&new_value[old_len], text, text_len);
203                 FREE(last_child->value);
204                 last_child->value = new_value;
205                 return 0;
206         }
207         if (!xml_new_node(element, XML_TEXT_NODE, NULL, 0, text, text_len))
208                 return WIMLIB_ERR_NOMEM;
209         return 0;
210 }
211
212 /* Find the attribute with the given @name on @element. */
213 struct xml_node *
214 xml_get_attrib(const struct xml_node *element, const tchar *name)
215 {
216         struct xml_node *child;
217
218         xml_node_for_each_child(element, child) {
219                 if (child->type == XML_ATTRIBUTE_NODE &&
220                     !tstrcmp(child->name, name))
221                         return child;
222         }
223         return NULL;
224 }
225
226 /* Set the attribute @name=@value on the given @element. */
227 int
228 xml_set_attrib(struct xml_node *element, const tchar *name, const tchar *value)
229 {
230         struct xml_node *attrib = xml_new_node(NULL, XML_ATTRIBUTE_NODE,
231                                                name, tstrlen(name),
232                                                value, tstrlen(value));
233         if (!attrib)
234                 return WIMLIB_ERR_NOMEM;
235         xml_replace_child(element, attrib);
236         return 0;
237 }
238
239 /*
240  * Add the ELEMENT or ATTRIBUTE node @replacement under the ELEMENT @parent,
241  * replacing any node with the same type and name that already exists.
242  */
243 void
244 xml_replace_child(struct xml_node *parent, struct xml_node *replacement)
245 {
246         struct xml_node *child;
247
248         xml_unlink_node(replacement); /* Shouldn't be needed, but be safe. */
249
250         xml_node_for_each_child(parent, child) {
251                 if (child->type == replacement->type &&
252                     !tstrcmp(child->name, replacement->name)) {
253                         list_replace(&child->sibling_link,
254                                      &replacement->sibling_link);
255                         replacement->parent = parent;
256                         child->parent = NULL;
257                         xml_free_node(child);
258                         return;
259                 }
260         }
261         xml_add_child(parent, replacement);
262 }
263
264 struct xml_node *
265 xml_clone_tree(struct xml_node *orig)
266 {
267         struct xml_node *clone, *orig_child, *clone_child;
268
269         clone = xml_new_node(NULL, orig->type,
270                         orig->name, orig->name ? tstrlen(orig->name) : 0,
271                         orig->value, orig->value ? tstrlen(orig->value) : 0);
272         if (!clone)
273                 return NULL;
274         xml_node_for_each_child(orig, orig_child) {
275                 clone_child = xml_clone_tree(orig_child);
276                 if (!clone_child)
277                         goto oom;
278                 xml_add_child(clone, clone_child);
279         }
280         return clone;
281
282 oom:
283         xml_free_node(clone);
284         return NULL;
285 }
286
287 /*----------------------------------------------------------------------------*
288  *                           XML string validation                            *
289  *----------------------------------------------------------------------------*/
290
291 /*
292  * Functions that check for legal names and values in XML 1.0.  These are
293  * currently slightly over-lenient, as they allow everything non-ASCII.  These
294  * are also not currently used by the XML parser to reject non-well-formed
295  * documents, but rather just by the user of the XML processor (xml.c) in order
296  * to avoid introducing illegal names and values into the document.
297  */
298
299 static inline bool
300 is_whitespace(tchar c)
301 {
302         return c == ' ' || c == '\n' || c == '\r' || c == '\t';
303 }
304
305 static inline bool
306 is_name_start_char(tchar c)
307 {
308         return (c & 0x7f) != c /* overly lenient for now */ ||
309                 (c >= 'A' && c <= 'Z') ||
310                 (c >= 'a' && c <= 'z') ||
311                 c == ':' || c == '_';
312 }
313
314 static inline bool
315 is_name_char(tchar c)
316 {
317         return is_name_start_char(c) ||
318                 (c >= '0' && c <= '9') || c == '-' || c == '.';
319 }
320
321 bool
322 xml_legal_name(const tchar *p)
323 {
324         if (!is_name_start_char(*p))
325                 return false;
326         for (p = p + 1; *p; p++) {
327                 if (!is_name_char(*p))
328                         return false;
329         }
330         return true;
331 }
332
333 bool
334 xml_legal_value(const tchar *p)
335 {
336         for (; *p; p++) {
337                 if (*p < 0x20 && !is_whitespace(*p))
338                         return false;
339         }
340         return true;
341 }
342
343 #if TCHAR_IS_UTF16LE
344 #define BYTE_ORDER_MARK (tchar[]){ 0xfeff, 0 }
345 #else
346 #define BYTE_ORDER_MARK "\xEF\xBB\xBF"
347 #endif
348
349 /*----------------------------------------------------------------------------*
350  *                               XML parsing                                  *
351  *----------------------------------------------------------------------------*/
352
353 #define CHECK(cond)     if (!(cond)) goto bad
354
355 static inline void
356 skip_whitespace(const tchar **pp)
357 {
358         const tchar *p = *pp;
359
360         while (is_whitespace(*p))
361                 p++;
362         *pp = p;
363 }
364
365 static inline bool
366 skip_string(const tchar **pp, const tchar *str)
367 {
368         const tchar *p = *pp;
369         size_t len = tstrlen(str);
370
371         if (tstrncmp(p, str, len))
372                 return false;
373         *pp = p + len;
374         return true;
375 }
376
377 static inline bool
378 find_and_skip(const tchar **pp, const tchar *str)
379 {
380         const tchar *p = *pp;
381
382         p = tstrstr(p, str);
383         if (!p)
384                 return false;
385         *pp = p + tstrlen(str);
386         return true;
387 }
388
389 static bool
390 skip_misc(const tchar **pp)
391 {
392         const tchar *p = *pp, *prev_p;
393
394         do {
395                 prev_p = p;
396                 skip_whitespace(&p);
397                 /* Discard XML declaration and top-level PIs for now. */
398                 if (skip_string(&p, T("<?")) && !find_and_skip(&p, T("?>")))
399                         return false;
400                 /* Discard DOCTYPE declaration for now. */
401                 if (skip_string(&p, T("<!DOCTYPE")) && !find_and_skip(&p, T(">")))
402                         return false;
403                 /* Discard top-level comments for now. */
404                 if (skip_string(&p, T("<!--")) && !find_and_skip(&p, T("-->")))
405                         return false;
406         } while (p != prev_p);
407         *pp = p;
408         return true;
409 }
410
411 static inline const tchar *
412 get_escape_seq(tchar c)
413 {
414         switch (c) {
415         case '<':
416                 return T("&lt;");
417         case '>':
418                 return T("&gt;");
419         case '&':
420                 return T("&amp;");
421         case '\'':
422                 return T("&apos;");
423         case '"':
424                 return T("&quot;");
425         }
426         return NULL;
427 }
428
429 /* Note: 'str' must be NUL-terminated, but only 'len' chars are used. */
430 static int
431 unescape_string(const tchar *str, size_t len, tchar **unescaped_ret)
432 {
433         const tchar *in_p = str;
434         tchar *unescaped, *out_p;
435
436         unescaped = CALLOC(len + 1, sizeof(str[0]));
437         if (!unescaped)
438                 return WIMLIB_ERR_NOMEM;
439         out_p = unescaped;
440         while (in_p < &str[len]) {
441                 if (*in_p != '&')
442                         *out_p++ = *in_p++;
443                 else if (skip_string(&in_p, T("&lt;")))
444                         *out_p++ = '<';
445                 else if (skip_string(&in_p, T("&gt;")))
446                         *out_p++ = '>';
447                 else if (skip_string(&in_p, T("&amp;")))
448                         *out_p++ = '&';
449                 else if (skip_string(&in_p, T("&apos;")))
450                         *out_p++ = '\'';
451                 else if (skip_string(&in_p, T("&quot;")))
452                         *out_p++ = '"';
453                 else
454                         goto bad;
455         }
456         if (in_p > &str[len])
457                 goto bad;
458         *unescaped_ret = unescaped;
459         return 0;
460
461 bad:
462         ERROR("Error unescaping string '%.*"TS"'", (int)len, str);
463         FREE(unescaped);
464         return WIMLIB_ERR_XML;
465 }
466
467 static int
468 parse_element(const tchar **pp, struct xml_node *parent, int depth,
469               struct xml_node **node_ret);
470
471 static int
472 parse_contents(const tchar **pp, struct xml_node *element, int depth)
473 {
474         const tchar *p = *pp;
475         int ret;
476
477         for (;;) {
478                 const tchar *raw_text = p;
479                 tchar *text;
480
481                 for (; *p != '<'; p++) {
482                         if (*p == '\0')
483                                 return WIMLIB_ERR_XML;
484                 }
485                 if (p > raw_text) {
486                         ret = unescape_string(raw_text, p - raw_text, &text);
487                         if (ret)
488                                 return ret;
489                         ret = xml_element_append_text(element, text,
490                                                       tstrlen(text));
491                         FREE(text);
492                         if (ret)
493                                 return ret;
494                 }
495                 if (p[1] == '/') {
496                         break; /* Reached the end tag of @element */
497                 } else if (p[1] == '?') {
498                         /* Discard processing instructions for now. */
499                         p += 2;
500                         if (!find_and_skip(&p, T("?>")))
501                                 return WIMLIB_ERR_XML;
502                         continue;
503                 } else if (p[1] == '!') {
504                         if (skip_string(&p, T("<![CDATA["))) {
505                                 raw_text = p;
506                                 if (!find_and_skip(&p, T("]]>")))
507                                         return WIMLIB_ERR_XML;
508                                 ret = xml_element_append_text(element, raw_text,
509                                                               p - 3 - raw_text);
510                                 if (ret)
511                                         return ret;
512                                 continue;
513                         } else if (skip_string(&p, T("<!--"))) {
514                                 /* Discard comments for now. */
515                                 if (!find_and_skip(&p, T("-->")))
516                                         return WIMLIB_ERR_XML;
517                                 continue;
518                         }
519                         return WIMLIB_ERR_XML;
520                 }
521                 ret = parse_element(&p, element, depth + 1, NULL);
522                 if (ret)
523                         return ret;
524         }
525         *pp = p;
526         return 0;
527 }
528
529 static int
530 parse_element(const tchar **pp, struct xml_node *parent, int depth,
531               struct xml_node **element_ret)
532 {
533         const tchar *p = *pp;
534         struct xml_node *element = NULL;
535         const tchar *name_start;
536         size_t name_len;
537         int ret;
538
539         /* Parse the start tag. */
540         CHECK(depth < 50);
541         CHECK(*p == '<');
542         p++;
543         name_start = p;
544         while (!is_whitespace(*p) && *p != '>' && *p != '\0')
545                 p++;
546         name_len = p - name_start;
547         CHECK(name_len > 0);
548         element = xml_new_node(parent, XML_ELEMENT_NODE, name_start, name_len,
549                                NULL, 0);
550         if (!element) {
551                 ret = WIMLIB_ERR_NOMEM;
552                 goto error;
553         }
554         /* Parse the attributes list within the start tag. */
555         while (is_whitespace(*p)) {
556                 const tchar *attr_name_start, *attr_value_start;
557                 size_t attr_name_len, attr_value_len;
558                 tchar *attr_value;
559                 tchar quote;
560
561                 skip_whitespace(&p);
562                 if (*p == '/' || *p == '>')
563                         break;
564                 attr_name_start = p;
565                 while (*p != '=' && !is_whitespace(*p) && *p != '\0')
566                         p++;
567                 attr_name_len = p - attr_name_start;
568                 skip_whitespace(&p);
569                 CHECK(attr_name_len > 0 && *p == '=');
570                 p++;
571                 skip_whitespace(&p);
572                 quote = *p;
573                 CHECK(quote == '\'' || quote == '"');
574                 attr_value_start = ++p;
575                 while (*p != quote && *p != '\0')
576                         p++;
577                 CHECK(*p == quote);
578                 attr_value_len = p - attr_value_start;
579                 p++;
580                 ret = unescape_string(attr_value_start, attr_value_len,
581                                       &attr_value);
582                 if (ret)
583                         goto error;
584                 ret = xml_new_node(element, XML_ATTRIBUTE_NODE,
585                                    attr_name_start, attr_name_len,
586                                    attr_value, tstrlen(attr_value))
587                         ? 0 : WIMLIB_ERR_NOMEM;
588                 FREE(attr_value);
589                 if (ret)
590                         goto error;
591         }
592         if (*p == '/') {
593                 /* Closing an empty element tag */
594                 p++;
595         } else {
596                 /* Closing the start tag */
597                 CHECK(*p == '>');
598                 p++;
599                 /* Parse the contents, then the end tag. */
600                 ret = parse_contents(&p, element, depth);
601                 if (ret)
602                         goto error;
603                 CHECK(*p == '<');
604                 p++;
605                 CHECK(*p == '/');
606                 p++;
607                 CHECK(!tstrncmp(p, name_start, name_len));
608                 p += name_len;
609                 skip_whitespace(&p);
610         }
611         CHECK(*p == '>');
612         p++;
613         *pp = p;
614         if (element_ret)
615                 *element_ret = element;
616         return 0;
617
618 error:
619         xml_free_node(element);
620         return ret;
621
622 bad:
623         ret = WIMLIB_ERR_XML;
624         goto error;
625 }
626
627 /*
628  * Deserialize an XML document and return its root node in @doc_ret.  The
629  * document must be given as a NUL-terminated string of 'tchar', i.e. UTF-16LE
630  * in Windows builds and UTF-8 everywhere else.
631  */
632 int
633 xml_parse_document(const tchar *p, struct xml_node **doc_ret)
634 {
635         int ret;
636         struct xml_node *doc;
637
638         skip_string(&p, BYTE_ORDER_MARK);
639         if (!skip_misc(&p))
640                 return WIMLIB_ERR_XML;
641         ret = parse_element(&p, NULL, 0, &doc);
642         if (ret)
643                 return ret;
644         if (!skip_misc(&p) || *p) {
645                 xml_free_node(doc);
646                 return WIMLIB_ERR_XML;
647         }
648         *doc_ret = doc;
649         return 0;
650 }
651
652 /*----------------------------------------------------------------------------*
653  *                               XML writing                                  *
654  *----------------------------------------------------------------------------*/
655
656 static void
657 xml_write(struct xml_out_buf *buf, const tchar *str, size_t len)
658 {
659         if (buf->count + len + 1 > buf->capacity) {
660                 size_t new_capacity = max(buf->capacity * 2, 4096);
661                 tchar *new_buf = REALLOC(buf->buf,
662                                          new_capacity * sizeof(str[0]));
663                 if (!new_buf) {
664                         buf->oom = true;
665                         return;
666                 }
667                 buf->buf = new_buf;
668                 buf->capacity = new_capacity;
669         }
670         tmemcpy(&buf->buf[buf->count], str, len);
671         buf->count += len;
672 }
673
674 static void
675 xml_puts(struct xml_out_buf *buf, const tchar *str)
676 {
677         xml_write(buf, str, tstrlen(str));
678 }
679
680 static void
681 xml_escape_and_puts(struct xml_out_buf *buf, const tchar *str)
682 {
683         const tchar *p = str, *saved, *seq = NULL;
684
685         for (;; p++) {
686                 for (saved = p; *p && (seq = get_escape_seq(*p)) == NULL; p++)
687                         ;
688                 xml_write(buf, saved, p - saved);
689                 if (!*p)
690                         return;
691                 xml_puts(buf, seq);
692         }
693 }
694
695 static void
696 xml_write_element(struct xml_node *element, struct xml_out_buf *buf)
697 {
698         struct xml_node *child;
699
700         /* Write the start tag. */
701         xml_puts(buf, T("<"));
702         xml_puts(buf, element->name);
703         xml_node_for_each_child(element, child) {
704                 if (child->type == XML_ATTRIBUTE_NODE) {
705                         xml_puts(buf, T(" "));
706                         xml_puts(buf, child->name);
707                         xml_puts(buf, T("=\""));
708                         xml_escape_and_puts(buf, child->value);
709                         xml_puts(buf, T("\""));
710                 }
711         }
712         xml_puts(buf, T(">"));
713
714         /* Write the contents. */
715         xml_node_for_each_child(element, child) {
716                 if (child->type == XML_TEXT_NODE)
717                         xml_escape_and_puts(buf, child->value);
718                 else if (child->type == XML_ELEMENT_NODE)
719                         xml_write_element(child, buf);
720         }
721
722         /* Write the end tag. */
723         xml_puts(buf, T("</"));
724         xml_puts(buf, element->name);
725         xml_puts(buf, T(">"));
726 }
727
728 /*
729  * Serialize the document @doc into @buf as a NUL-terminated string of 'tchar',
730  * i.e. UTF-16LE in Windows builds and UTF-8 everywhere else.  A byte order mark
731  * (BOM) is included, as this is needed for compatibility with WIMGAPI.
732  */
733 int
734 xml_write_document(struct xml_node *doc, struct xml_out_buf *buf)
735 {
736         xml_puts(buf, BYTE_ORDER_MARK);
737         xml_write_element(doc, buf);
738         if (buf->oom)
739                 return WIMLIB_ERR_NOMEM;
740         buf->buf[buf->count] = '\0';
741         return 0;
742 }
743
744 /*----------------------------------------------------------------------------*
745  *                              Test support                                  *
746  *----------------------------------------------------------------------------*/
747
748 #ifdef ENABLE_TEST_SUPPORT
749 WIMLIBAPI int
750 wimlib_parse_and_write_xml_doc(const tchar *in, tchar **out_ret)
751 {
752         struct xml_node *doc;
753         struct xml_out_buf buf = {};
754         int ret;
755
756         ret = xml_parse_document(in, &doc);
757         if (ret)
758                 return ret;
759         ret = xml_write_document(doc, &buf);
760         xml_free_node(doc);
761         *out_ret = buf.buf;
762         return ret;
763 }
764 #endif /* ENABLE_TEST_SUPPORT */