diff options
| author | 2026-02-25 02:27:00 +0100 | |
|---|---|---|
| committer | 2026-02-25 02:27:00 +0100 | |
| commit | 6e03a1df6d1aa87c8f9e2b35f1a2ca60feca1c0e (patch) | |
| tree | a571c74147d33da0a4dbfade178180c692c60447 | |
| parent | f5e74fc1e007b8f625d91e40c160785580de8f60 (diff) | |
| download | libplist-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-- | .gitignore | 10 | ||||
| -rw-r--r-- | src/xplist.c | 32 | ||||
| -rw-r--r-- | test/Makefile.am | 9 | ||||
| -rwxr-xr-x | test/xml_behavior.test | 2 | ||||
| -rw-r--r-- | test/xml_behavior_test.c | 167 |
5 files changed, 203 insertions, 17 deletions
| @@ -54,6 +54,7 @@ test/plist_btest | |||
| 54 | test/plist_jtest | 54 | test/plist_jtest |
| 55 | test/plist_otest | 55 | test/plist_otest |
| 56 | test/integer_set_test | 56 | test/integer_set_test |
| 57 | test/xml_behavior_test | ||
| 57 | test/data/*.out | 58 | test/data/*.out |
| 58 | test/*.trs | 59 | test/*.trs |
| 59 | cython/Makefile | 60 | cython/Makefile |
| @@ -62,3 +63,12 @@ cython/.deps | |||
| 62 | cython/.libs | 63 | cython/.libs |
| 63 | cython/plist.c | 64 | cython/plist.c |
| 64 | test-driver | 65 | test-driver |
| 66 | |||
| 67 | # Generated test output files | ||
| 68 | test/data/*.test.bin | ||
| 69 | test/data/*.test.signed.bin | ||
| 70 | test/data/*.test.unsigned.bin | ||
| 71 | test/data/*.test.unsigned.xml | ||
| 72 | test/data/*.test.tz*.bin | ||
| 73 | test/data/*.test.tz*.xml | ||
| 74 | test/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 | ||
| 18 | plist_cmp_SOURCES = plist_cmp.c | 19 | plist_cmp_SOURCES = plist_cmp.c |
| 19 | plist_cmp_LDADD = \ | 20 | plist_cmp_LDADD = \ |
| @@ -38,6 +39,9 @@ plist_jtest_LDADD = $(top_builddir)/src/libplist-2.0.la | |||
| 38 | plist_otest_SOURCES = plist_otest.c | 39 | plist_otest_SOURCES = plist_otest.c |
| 39 | plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la | 40 | plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la |
| 40 | 41 | ||
| 42 | xml_behavior_test_SOURCES = xml_behavior_test.c | ||
| 43 | xml_behavior_test_LDADD = $(top_builddir)/src/libplist-2.0.la | ||
| 44 | |||
| 41 | TESTS = \ | 45 | TESTS = \ |
| 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 | ||
| 84 | EXTRA_DIST = \ | 89 | EXTRA_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 | |||
| 29 | static 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 | |||
| 85 | static 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 | |||
| 108 | static 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 | |||
| 130 | static 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 | |||
| 157 | int 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 | } | ||
