summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Sami Kortelainen2026-02-25 02:27:00 +0100
committerGravatar Nikias Bassen2026-02-25 02:27:00 +0100
commit6e03a1df6d1aa87c8f9e2b35f1a2ca60feca1c0e (patch)
treea571c74147d33da0a4dbfade178180c692c60447
parentf5e74fc1e007b8f625d91e40c160785580de8f60 (diff)
downloadlibplist-6e03a1df6d1aa87c8f9e2b35f1a2ca60feca1c0e.tar.gz
libplist-6e03a1df6d1aa87c8f9e2b35f1a2ca60feca1c0e.tar.bz2
xplist: Enforce single root value inside <plist>
Ensure that XML property lists contain exactly one root value inside the <plist> element and reject any additional value nodes before </plist>. Add tests covering root value handling and nested CF$UID conversion behavior. Co-authored-by: Sami Kortelainen <sami.kortelainen@piceasoft.com> Co-authored-by: Nikias Bassen <nikias@gmx.li>
-rw-r--r--.gitignore10
-rw-r--r--src/xplist.c32
-rw-r--r--test/Makefile.am9
-rwxr-xr-xtest/xml_behavior.test2
-rw-r--r--test/xml_behavior_test.c167
5 files changed, 203 insertions, 17 deletions
diff --git a/.gitignore b/.gitignore
index e63b0b6..a59b057 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ test/plist_btest
54test/plist_jtest 54test/plist_jtest
55test/plist_otest 55test/plist_otest
56test/integer_set_test 56test/integer_set_test
57test/xml_behavior_test
57test/data/*.out 58test/data/*.out
58test/*.trs 59test/*.trs
59cython/Makefile 60cython/Makefile
@@ -62,3 +63,12 @@ cython/.deps
62cython/.libs 63cython/.libs
63cython/plist.c 64cython/plist.c
64test-driver 65test-driver
66
67# Generated test output files
68test/data/*.test.bin
69test/data/*.test.signed.bin
70test/data/*.test.unsigned.bin
71test/data/*.test.unsigned.xml
72test/data/*.test.tz*.bin
73test/data/*.test.tz*.xml
74test/data/*.test.xml
diff --git a/src/xplist.c b/src/xplist.c
index a445dc5..de5227a 100644
--- a/src/xplist.c
+++ b/src/xplist.c
@@ -1170,8 +1170,9 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
1170 ctx->pos++; 1170 ctx->pos++;
1171 if (!strcmp(tag, "plist")) { 1171 if (!strcmp(tag, "plist")) {
1172 if (!node_path && *plist) { 1172 if (!node_path && *plist) {
1173 /* we don't allow another top-level <plist> */ 1173 PLIST_XML_ERR("Multiple top-level <plist> elements encountered\n");
1174 break; 1174 ctx->err = PLIST_ERR_PARSE;
1175 goto err_out;
1175 } 1176 }
1176 if (is_empty) { 1177 if (is_empty) {
1177 PLIST_XML_ERR("Empty plist tag\n"); 1178 PLIST_XML_ERR("Empty plist tag\n");
@@ -1403,12 +1404,6 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
1403 data->length = length; 1404 data->length = length;
1404 } 1405 }
1405 } else { 1406 } else {
1406 if (!strcmp(tag, "key") && !keyname && parent && (plist_get_node_type(parent) == PLIST_DICT)) {
1407 keyname = strdup("");
1408 plist_free(subnode);
1409 subnode = NULL;
1410 continue;
1411 }
1412 data->strval = strdup(""); 1407 data->strval = strdup("");
1413 data->length = 0; 1408 data->length = 0;
1414 } 1409 }
@@ -1501,14 +1496,15 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
1501 } 1496 }
1502 if (subnode && !closing_tag) { 1497 if (subnode && !closing_tag) {
1503 if (!*plist) { 1498 if (!*plist) {
1504 /* first node, make this node the parent node */ 1499 /* first value node inside <plist> */
1505 *plist = subnode; 1500 *plist = subnode;
1506 if (data->type != PLIST_DICT && data->type != PLIST_ARRAY) { 1501
1507 /* if the first node is not a structered node, we're done */ 1502 if (data->type == PLIST_DICT || data->type == PLIST_ARRAY) {
1508 subnode = NULL; 1503 parent = subnode;
1509 goto err_out; 1504 } else {
1505 /* scalar root: keep parsing until </plist> */
1506 parent = NULL;
1510 } 1507 }
1511 parent = subnode;
1512 } else if (parent) { 1508 } else if (parent) {
1513 switch (plist_get_node_type(parent)) { 1509 switch (plist_get_node_type(parent)) {
1514 case PLIST_DICT: 1510 case PLIST_DICT:
@@ -1528,6 +1524,11 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
1528 ctx->err = PLIST_ERR_PARSE; 1524 ctx->err = PLIST_ERR_PARSE;
1529 goto err_out; 1525 goto err_out;
1530 } 1526 }
1527 } else {
1528 /* We already produced root, and we're not inside a container */
1529 PLIST_XML_ERR("Unexpected tag <%s> found while </plist> is expected\n", tag);
1530 ctx->err = PLIST_ERR_PARSE;
1531 goto err_out;
1531 } 1532 }
1532 if (!is_empty && (data->type == PLIST_DICT || data->type == PLIST_ARRAY)) { 1533 if (!is_empty && (data->type == PLIST_DICT || data->type == PLIST_ARRAY)) {
1533 if (depth >= PLIST_MAX_NESTING_DEPTH) { 1534 if (depth >= PLIST_MAX_NESTING_DEPTH) {
@@ -1547,6 +1548,8 @@ static plist_err_t node_from_xml(parse_ctx ctx, plist_t *plist)
1547 1548
1548 depth++; 1549 depth++;
1549 parent = subnode; 1550 parent = subnode;
1551 } else {
1552 /* If we inserted a child scalar into a container, nothing to push. */
1550 } 1553 }
1551 subnode = NULL; 1554 subnode = NULL;
1552 } 1555 }
@@ -1587,7 +1590,6 @@ handle_closing:
1587 node_path = (struct node_path_item*)node_path->prev; 1590 node_path = (struct node_path_item*)node_path->prev;
1588 free(path_item); 1591 free(path_item);
1589 parent = (parent) ? ((node_t)parent)->parent : NULL; 1592 parent = (parent) ? ((node_t)parent)->parent : NULL;
1590 /* parent can be NULL when we just closed the root node; keep parsing */
1591 } 1593 }
1592 free(keyname); 1594 free(keyname);
1593 keyname = NULL; 1595 keyname = NULL;
diff --git a/test/Makefile.am b/test/Makefile.am
index a4191c4..f9f21e4 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -13,7 +13,8 @@ noinst_PROGRAMS = \
13 integer_set_test \ 13 integer_set_test \
14 plist_btest \ 14 plist_btest \
15 plist_jtest \ 15 plist_jtest \
16 plist_otest 16 plist_otest \
17 xml_behavior_test
17 18
18plist_cmp_SOURCES = plist_cmp.c 19plist_cmp_SOURCES = plist_cmp.c
19plist_cmp_LDADD = \ 20plist_cmp_LDADD = \
@@ -38,6 +39,9 @@ plist_jtest_LDADD = $(top_builddir)/src/libplist-2.0.la
38plist_otest_SOURCES = plist_otest.c 39plist_otest_SOURCES = plist_otest.c
39plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la 40plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la
40 41
42xml_behavior_test_SOURCES = xml_behavior_test.c
43xml_behavior_test_LDADD = $(top_builddir)/src/libplist-2.0.la
44
41TESTS = \ 45TESTS = \
42 empty.test \ 46 empty.test \
43 small.test \ 47 small.test \
@@ -79,7 +83,8 @@ TESTS = \
79 ostep2.test \ 83 ostep2.test \
80 ostep-strings.test \ 84 ostep-strings.test \
81 ostep-comments.test \ 85 ostep-comments.test \
82 ostep-invalid-types.test 86 ostep-invalid-types.test \
87 xml_behavior.test
83 88
84EXTRA_DIST = \ 89EXTRA_DIST = \
85 $(TESTS) \ 90 $(TESTS) \
diff --git a/test/xml_behavior.test b/test/xml_behavior.test
new file mode 100755
index 0000000..81d8dd0
--- /dev/null
+++ b/test/xml_behavior.test
@@ -0,0 +1,2 @@
1## -*- sh -*-
2$top_builddir/test/xml_behavior_test
diff --git a/test/xml_behavior_test.c b/test/xml_behavior_test.c
new file mode 100644
index 0000000..94d8a7f
--- /dev/null
+++ b/test/xml_behavior_test.c
@@ -0,0 +1,167 @@
1/*
2 * xml_behavior_test.c
3 *
4 * Tests XML parser behavior for correctness and specification compliance:
5 *
6 * 1) A <plist> element must contain exactly one root value node.
7 * Any additional value nodes after the first root object must
8 * cause parsing to fail.
9 *
10 * 2) Dictionaries of the form:
11 * <dict>
12 * <key>CF$UID</key>
13 * <integer>...</integer>
14 * </dict>
15 * must be converted to PLIST_UID nodes during XML parsing,
16 * including when they appear nested inside other containers.
17 *
18 * These tests ensure proper root handling and UID node conversion
19 * when parsing XML property lists.
20 */
21
22#include <stdio.h>
23#include <string.h>
24#include <stdint.h>
25#include <inttypes.h>
26
27#include "plist/plist.h"
28
29static int test_nested_cfuid_converts_to_uid(void)
30{
31 const char *xml =
32 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
33 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
34 "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
35 "<plist version=\"1.0\">"
36 " <dict>"
37 " <key>obj</key>"
38 " <dict>"
39 " <key>CF$UID</key>"
40 " <integer>7</integer>"
41 " </dict>"
42 " </dict>"
43 "</plist>";
44
45 plist_t root = NULL;
46 plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
47 if (err != PLIST_ERR_SUCCESS || !root) {
48 fprintf(stderr, "nested CF$UID: plist_from_xml failed (err=%d)\n", err);
49 plist_free(root);
50 return 0;
51 }
52
53 if (plist_get_node_type(root) != PLIST_DICT) {
54 fprintf(stderr, "nested CF$UID: root is not dict\n");
55 plist_free(root);
56 return 0;
57 }
58
59 plist_t obj = plist_dict_get_item(root, "obj");
60 if (!obj) {
61 fprintf(stderr, "nested CF$UID: missing key 'obj'\n");
62 plist_free(root);
63 return 0;
64 }
65
66 if (plist_get_node_type(obj) != PLIST_UID) {
67 fprintf(stderr, "nested CF$UID: expected PLIST_UID, got %d\n",
68 plist_get_node_type(obj));
69 plist_free(root);
70 return 0;
71 }
72
73 uint64_t uid = 0;
74 plist_get_uid_val(obj, &uid);
75 if (uid != 7) {
76 fprintf(stderr, "nested CF$UID: expected uid=7, got %" PRIu64 "\n", uid);
77 plist_free(root);
78 return 0;
79 }
80
81 plist_free(root);
82 return 1;
83}
84
85static int test_extra_root_value_is_rejected(void)
86{
87 /* Two root values inside <plist> must be rejected */
88 const char *xml =
89 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
90 "<plist version=\"1.0\">"
91 " <string>one</string>"
92 " <string>two</string>"
93 "</plist>";
94
95 plist_t root = NULL;
96 plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
97
98 /* Must fail, and root must be NULL (consistent with other parsers) */
99 if (err == PLIST_ERR_SUCCESS || root != NULL) {
100 fprintf(stderr, "extra root value: expected failure, got err=%d root=%p\n",
101 err, (void*)root);
102 plist_free(root);
103 return 0;
104 }
105 return 1;
106}
107
108static int test_scalar_then_extra_node_is_rejected(void)
109{
110 /* Scalar root followed by another node must be rejected */
111 const char *xml =
112 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
113 "<plist version=\"1.0\">"
114 " <true/>"
115 " <dict><key>A</key><string>x</string></dict>"
116 "</plist>";
117
118 plist_t root = NULL;
119 plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
120
121 if (err == PLIST_ERR_SUCCESS || root != NULL) {
122 fprintf(stderr, "scalar then extra node: expected failure, got err=%d root=%p\n",
123 err, (void*)root);
124 plist_free(root);
125 return 0;
126 }
127 return 1;
128}
129
130static int test_scalar_with_comment_is_ok(void)
131{
132 /* Comment after the single root value is not an extra value node */
133 const char *xml =
134 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
135 "<plist version=\"1.0\">"
136 " <string>ok</string>"
137 " <!-- trailing comment -->"
138 "</plist>";
139
140 plist_t root = NULL;
141 plist_err_t err = plist_from_xml(xml, (uint32_t)strlen(xml), &root);
142 if (err != PLIST_ERR_SUCCESS || !root) {
143 fprintf(stderr, "scalar + comment: expected success, got err=%d\n", err);
144 plist_free(root);
145 return 0;
146 }
147 if (plist_get_node_type(root) != PLIST_STRING) {
148 fprintf(stderr, "scalar + comment: expected root string, got %d\n",
149 plist_get_node_type(root));
150 plist_free(root);
151 return 0;
152 }
153 plist_free(root);
154 return 1;
155}
156
157int main(void)
158{
159 int ok = 1;
160
161 ok &= test_nested_cfuid_converts_to_uid();
162 ok &= test_extra_root_value_is_rejected();
163 ok &= test_scalar_then_extra_node_is_rejected();
164 ok &= test_scalar_with_comment_is_ok();
165
166 return ok ? 0 : 1;
167}