]> wimlib.net Git - wimlib/blob - src/xmlproc.c
mount_image.c: add fallback definitions of RENAME_* constants
[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 https://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 /* Allow characters used in element "paths"; see do_xml_path_walk() */
322 static inline bool
323 is_path_char(tchar c)
324 {
325         return c == '/' || c == '[' || c == ']';
326 }
327
328 bool
329 xml_legal_path(const tchar *p)
330 {
331         if (!is_name_start_char(*p) && !is_path_char(*p))
332                 return false;
333         for (p = p + 1; *p; p++) {
334                 if (!is_name_char(*p) && !is_path_char(*p))
335                         return false;
336         }
337         return true;
338 }
339
340 bool
341 xml_legal_value(const tchar *p)
342 {
343         for (; *p; p++) {
344                 /* Careful: tchar can be signed. */
345                 if (*p > 0 && *p < 0x20 && !is_whitespace(*p))
346                         return false;
347         }
348         return true;
349 }
350
351 #if TCHAR_IS_UTF16LE
352 #define BYTE_ORDER_MARK (tchar[]){ 0xfeff, 0 }
353 #else
354 #define BYTE_ORDER_MARK "\xEF\xBB\xBF"
355 #endif
356
357 /*----------------------------------------------------------------------------*
358  *                               XML parsing                                  *
359  *----------------------------------------------------------------------------*/
360
361 #define CHECK(cond)     if (!(cond)) goto bad
362
363 static inline void
364 skip_whitespace(const tchar **pp)
365 {
366         const tchar *p = *pp;
367
368         while (is_whitespace(*p))
369                 p++;
370         *pp = p;
371 }
372
373 static inline bool
374 skip_string(const tchar **pp, const tchar *str)
375 {
376         const tchar *p = *pp;
377         size_t len = tstrlen(str);
378
379         if (tstrncmp(p, str, len))
380                 return false;
381         *pp = p + len;
382         return true;
383 }
384
385 static inline bool
386 find_and_skip(const tchar **pp, const tchar *str)
387 {
388         const tchar *p = *pp;
389
390         p = tstrstr(p, str);
391         if (!p)
392                 return false;
393         *pp = p + tstrlen(str);
394         return true;
395 }
396
397 static bool
398 skip_misc(const tchar **pp)
399 {
400         const tchar *p = *pp, *prev_p;
401
402         do {
403                 prev_p = p;
404                 skip_whitespace(&p);
405                 /* Discard XML declaration and top-level PIs for now. */
406                 if (skip_string(&p, T("<?")) && !find_and_skip(&p, T("?>")))
407                         return false;
408                 /* Discard DOCTYPE declaration for now. */
409                 if (skip_string(&p, T("<!DOCTYPE")) && !find_and_skip(&p, T(">")))
410                         return false;
411                 /* Discard top-level comments for now. */
412                 if (skip_string(&p, T("<!--")) && !find_and_skip(&p, T("-->")))
413                         return false;
414         } while (p != prev_p);
415         *pp = p;
416         return true;
417 }
418
419 static inline const tchar *
420 get_escape_seq(tchar c)
421 {
422         switch (c) {
423         case '<':
424                 return T("&lt;");
425         case '>':
426                 return T("&gt;");
427         case '&':
428                 return T("&amp;");
429         case '\'':
430                 return T("&apos;");
431         case '"':
432                 return T("&quot;");
433         }
434         return NULL;
435 }
436
437 /* Note: 'str' must be NUL-terminated, but only 'len' chars are used. */
438 static int
439 unescape_string(const tchar *str, size_t len, tchar **unescaped_ret)
440 {
441         const tchar *in_p = str;
442         tchar *unescaped, *out_p;
443
444         unescaped = CALLOC(len + 1, sizeof(str[0]));
445         if (!unescaped)
446                 return WIMLIB_ERR_NOMEM;
447         out_p = unescaped;
448         while (in_p < &str[len]) {
449                 if (*in_p != '&')
450                         *out_p++ = *in_p++;
451                 else if (skip_string(&in_p, T("&lt;")))
452                         *out_p++ = '<';
453                 else if (skip_string(&in_p, T("&gt;")))
454                         *out_p++ = '>';
455                 else if (skip_string(&in_p, T("&amp;")))
456                         *out_p++ = '&';
457                 else if (skip_string(&in_p, T("&apos;")))
458                         *out_p++ = '\'';
459                 else if (skip_string(&in_p, T("&quot;")))
460                         *out_p++ = '"';
461                 else
462                         goto bad;
463         }
464         if (in_p > &str[len])
465                 goto bad;
466         *unescaped_ret = unescaped;
467         return 0;
468
469 bad:
470         ERROR("Error unescaping string '%.*"TS"'", (int)len, str);
471         FREE(unescaped);
472         return WIMLIB_ERR_XML;
473 }
474
475 static int
476 parse_element(const tchar **pp, struct xml_node *parent, int depth,
477               struct xml_node **node_ret);
478
479 static int
480 parse_contents(const tchar **pp, struct xml_node *element, int depth)
481 {
482         const tchar *p = *pp;
483         int ret;
484
485         for (;;) {
486                 const tchar *raw_text = p;
487                 tchar *text;
488
489                 for (; *p != '<'; p++) {
490                         if (*p == '\0')
491                                 return WIMLIB_ERR_XML;
492                 }
493                 if (p > raw_text) {
494                         ret = unescape_string(raw_text, p - raw_text, &text);
495                         if (ret)
496                                 return ret;
497                         ret = xml_element_append_text(element, text,
498                                                       tstrlen(text));
499                         FREE(text);
500                         if (ret)
501                                 return ret;
502                 }
503                 if (p[1] == '/') {
504                         break; /* Reached the end tag of @element */
505                 } else if (p[1] == '?') {
506                         /* Discard processing instructions for now. */
507                         p += 2;
508                         if (!find_and_skip(&p, T("?>")))
509                                 return WIMLIB_ERR_XML;
510                         continue;
511                 } else if (p[1] == '!') {
512                         if (skip_string(&p, T("<![CDATA["))) {
513                                 raw_text = p;
514                                 if (!find_and_skip(&p, T("]]>")))
515                                         return WIMLIB_ERR_XML;
516                                 ret = xml_element_append_text(element, raw_text,
517                                                               p - 3 - raw_text);
518                                 if (ret)
519                                         return ret;
520                                 continue;
521                         } else if (skip_string(&p, T("<!--"))) {
522                                 /* Discard comments for now. */
523                                 if (!find_and_skip(&p, T("-->")))
524                                         return WIMLIB_ERR_XML;
525                                 continue;
526                         }
527                         return WIMLIB_ERR_XML;
528                 }
529                 ret = parse_element(&p, element, depth + 1, NULL);
530                 if (ret)
531                         return ret;
532         }
533         *pp = p;
534         return 0;
535 }
536
537 static int
538 parse_element(const tchar **pp, struct xml_node *parent, int depth,
539               struct xml_node **element_ret)
540 {
541         const tchar *p = *pp;
542         struct xml_node *element = NULL;
543         const tchar *name_start;
544         size_t name_len;
545         int ret;
546
547         /* Parse the start tag. */
548         CHECK(depth < 50);
549         CHECK(*p == '<');
550         p++;
551         name_start = p;
552         while (!is_whitespace(*p) && *p != '>' && *p != '/' && *p != '\0')
553                 p++;
554         name_len = p - name_start;
555         CHECK(name_len > 0);
556         element = xml_new_node(parent, XML_ELEMENT_NODE, name_start, name_len,
557                                NULL, 0);
558         if (!element) {
559                 ret = WIMLIB_ERR_NOMEM;
560                 goto error;
561         }
562         /* Parse the attributes list within the start tag. */
563         while (is_whitespace(*p)) {
564                 const tchar *attr_name_start, *attr_value_start;
565                 size_t attr_name_len, attr_value_len;
566                 tchar *attr_value;
567                 tchar quote;
568
569                 skip_whitespace(&p);
570                 if (*p == '/' || *p == '>')
571                         break;
572                 attr_name_start = p;
573                 while (*p != '=' && !is_whitespace(*p) && *p != '\0')
574                         p++;
575                 attr_name_len = p - attr_name_start;
576                 skip_whitespace(&p);
577                 CHECK(attr_name_len > 0 && *p == '=');
578                 p++;
579                 skip_whitespace(&p);
580                 quote = *p;
581                 CHECK(quote == '\'' || quote == '"');
582                 attr_value_start = ++p;
583                 while (*p != quote && *p != '\0')
584                         p++;
585                 CHECK(*p == quote);
586                 attr_value_len = p - attr_value_start;
587                 p++;
588                 ret = unescape_string(attr_value_start, attr_value_len,
589                                       &attr_value);
590                 if (ret)
591                         goto error;
592                 ret = xml_new_node(element, XML_ATTRIBUTE_NODE,
593                                    attr_name_start, attr_name_len,
594                                    attr_value, tstrlen(attr_value))
595                         ? 0 : WIMLIB_ERR_NOMEM;
596                 FREE(attr_value);
597                 if (ret)
598                         goto error;
599         }
600         if (*p == '/') {
601                 /* Closing an empty element tag */
602                 p++;
603         } else {
604                 /* Closing the start tag */
605                 CHECK(*p == '>');
606                 p++;
607                 /* Parse the contents, then the end tag. */
608                 ret = parse_contents(&p, element, depth);
609                 if (ret)
610                         goto error;
611                 CHECK(*p == '<');
612                 p++;
613                 CHECK(*p == '/');
614                 p++;
615                 CHECK(!tstrncmp(p, name_start, name_len));
616                 p += name_len;
617                 skip_whitespace(&p);
618         }
619         CHECK(*p == '>');
620         p++;
621         *pp = p;
622         if (element_ret)
623                 *element_ret = element;
624         return 0;
625
626 error:
627         xml_free_node(element);
628         return ret;
629
630 bad:
631         ret = WIMLIB_ERR_XML;
632         goto error;
633 }
634
635 /*
636  * Deserialize an XML document and return its root node in @doc_ret.  The
637  * document must be given as a NUL-terminated string of 'tchar', i.e. UTF-16LE
638  * in Windows builds and UTF-8 everywhere else.
639  */
640 int
641 xml_parse_document(const tchar *p, struct xml_node **doc_ret)
642 {
643         int ret;
644         struct xml_node *doc;
645
646         skip_string(&p, BYTE_ORDER_MARK);
647         if (!skip_misc(&p))
648                 return WIMLIB_ERR_XML;
649         ret = parse_element(&p, NULL, 0, &doc);
650         if (ret)
651                 return ret;
652         if (!skip_misc(&p) || *p) {
653                 xml_free_node(doc);
654                 return WIMLIB_ERR_XML;
655         }
656         *doc_ret = doc;
657         return 0;
658 }
659
660 /*----------------------------------------------------------------------------*
661  *                               XML writing                                  *
662  *----------------------------------------------------------------------------*/
663
664 static void
665 xml_write(struct xml_out_buf *buf, const tchar *str, size_t len)
666 {
667         if (buf->count + len + 1 > buf->capacity) {
668                 size_t new_capacity = max3(buf->count + len + 1,
669                                            buf->capacity * 2, 4096);
670                 tchar *new_buf = REALLOC(buf->buf,
671                                          new_capacity * sizeof(str[0]));
672                 if (!new_buf) {
673                         buf->oom = true;
674                         return;
675                 }
676                 buf->buf = new_buf;
677                 buf->capacity = new_capacity;
678         }
679         tmemcpy(&buf->buf[buf->count], str, len);
680         buf->count += len;
681 }
682
683 static void
684 xml_puts(struct xml_out_buf *buf, const tchar *str)
685 {
686         xml_write(buf, str, tstrlen(str));
687 }
688
689 static void
690 xml_escape_and_puts(struct xml_out_buf *buf, const tchar *str)
691 {
692         const tchar *p = str, *saved, *seq = NULL;
693
694         for (;; p++) {
695                 for (saved = p; *p && (seq = get_escape_seq(*p)) == NULL; p++)
696                         ;
697                 xml_write(buf, saved, p - saved);
698                 if (!*p)
699                         return;
700                 xml_puts(buf, seq);
701         }
702 }
703
704 static void
705 xml_write_element(struct xml_node *element, struct xml_out_buf *buf)
706 {
707         struct xml_node *child;
708
709         /* Write the start tag. */
710         xml_puts(buf, T("<"));
711         xml_puts(buf, element->name);
712         xml_node_for_each_child(element, child) {
713                 if (child->type == XML_ATTRIBUTE_NODE) {
714                         xml_puts(buf, T(" "));
715                         xml_puts(buf, child->name);
716                         xml_puts(buf, T("=\""));
717                         xml_escape_and_puts(buf, child->value);
718                         xml_puts(buf, T("\""));
719                 }
720         }
721         xml_puts(buf, T(">"));
722
723         /* Write the contents. */
724         xml_node_for_each_child(element, child) {
725                 if (child->type == XML_TEXT_NODE)
726                         xml_escape_and_puts(buf, child->value);
727                 else if (child->type == XML_ELEMENT_NODE)
728                         xml_write_element(child, buf);
729         }
730
731         /* Write the end tag. */
732         xml_puts(buf, T("</"));
733         xml_puts(buf, element->name);
734         xml_puts(buf, T(">"));
735 }
736
737 /*
738  * Serialize the document @doc into @buf as a NUL-terminated string of 'tchar',
739  * i.e. UTF-16LE in Windows builds and UTF-8 everywhere else.  A byte order mark
740  * (BOM) is included, as this is needed for compatibility with WIMGAPI.
741  */
742 int
743 xml_write_document(struct xml_node *doc, struct xml_out_buf *buf)
744 {
745         xml_puts(buf, BYTE_ORDER_MARK);
746         xml_write_element(doc, buf);
747         if (buf->oom)
748                 return WIMLIB_ERR_NOMEM;
749         buf->buf[buf->count] = '\0';
750         return 0;
751 }
752
753 /*----------------------------------------------------------------------------*
754  *                              Test support                                  *
755  *----------------------------------------------------------------------------*/
756
757 #ifdef ENABLE_TEST_SUPPORT
758 WIMLIBAPI int
759 wimlib_parse_and_write_xml_doc(const tchar *in, tchar **out_ret)
760 {
761         struct xml_node *doc;
762         struct xml_out_buf buf = {};
763         int ret;
764
765         ret = xml_parse_document(in, &doc);
766         if (ret)
767                 return ret;
768         ret = xml_write_document(doc, &buf);
769         xml_free_node(doc);
770         *out_ret = buf.buf;
771         return ret;
772 }
773 #endif /* ENABLE_TEST_SUPPORT */