From 3b4d715e988ba1642dbea093c28654923b30ea82 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 9 Sep 2026 20:26:26 -0700 Subject: [PATCH] XML: mutate XML nodes in place Previously, text and structural mutators copied and replaced the receiver's libxml2 node. Existing XMLNode wrappers continued to point at the detached original, while subsequent lookups returned the replacement. Wrappers for children preserved by the operation likewise pointed into the detached old subtree. In QuickJS, mutating the detached receiver again could dereference the NULL result of xmlReplaceNode(). The fix is to modify the existing receiver's child list directly. Existing wrappers and subsequent lookups therefore continue to refer to the same libxml2 node. Removed child lists remain readable until the QuickJS document or njs VM is destroyed. A node is considered live only when following its parent links reaches its xmlDoc. Replacement children and cleanup records are prepared before the live tree is modified. Changed behaviour: - A removed child has an undefined $parent, while its descendants retain their internal parent links. - Mutating or canonicalizing a removed XMLNode throws TypeError. - Text and attribute values must be primitive strings; null and undefined retain their deletion semantics. - C14N rejects an excluding node from another document or a detached list. - addChild() and $tags reject namespace-bearing or unsupported source trees before mutation. - Named $tags assignments are consistently rejected by both engines. While here, use an unsigned QuickJS $tags array length, support assigning $tags on an empty njs XMLNode, and make deleting $tags from an empty node a no-op. --- external/njs_xml_module.c | 456 ++++++++++++++++++++++++-------------- external/qjs_xml_module.c | 416 +++++++++++++++++++++++++--------- test/xml/xml.t.mjs | 356 +++++++++++++++++++++++++++-- 3 files changed, 933 insertions(+), 295 deletions(-) diff --git a/external/njs_xml_module.c b/external/njs_xml_module.c index f37f8879..08979bec 100644 --- a/external/njs_xml_module.c +++ b/external/njs_xml_module.c @@ -104,12 +104,14 @@ static njs_int_t njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, njs_value_t *setval, njs_value_t *retval); static xmlNode *njs_xml_external_node(njs_vm_t *vm, njs_value_t *value); -static njs_int_t njs_xml_str_to_c_string(njs_vm_t *vm, njs_str_t *str, +static const u_char *njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst, size_t size); -static const u_char *njs_xml_value_to_c_string(njs_vm_t *vm, njs_value_t *value, - u_char *dst, size_t size); -static njs_int_t njs_xml_replace_node(njs_vm_t *vm, njs_value_t *value, - xmlNode *current); +static njs_bool_t njs_xml_node_is_live(xmlNode *node); +static xmlNode *njs_xml_list_detach(xmlNode *parent, njs_str_t *name); +static njs_int_t njs_xml_list_retire(njs_vm_t *vm, xmlNode *parent, + njs_str_t *name); +static void njs_xml_list_append(xmlNode *parent, xmlNode *node); +static njs_bool_t njs_xml_tree_has_namespaces(xmlNode *node); static void njs_xml_node_cleanup(void *data); static void njs_xml_doc_cleanup(void *data); @@ -725,11 +727,10 @@ static njs_int_t njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - xmlNode *current, *node, *copy, *rnode; - njs_int_t ret; + xmlNode *current, *node; current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); - if (njs_slow_path(current == NULL)) { + if (njs_slow_path(!njs_xml_node_is_live(current))) { njs_vm_type_error(vm, "\"this\" is not a XMLNode object"); return NJS_ERROR; } @@ -740,40 +741,22 @@ njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args, return NJS_ERROR; } - copy = xmlDocCopyNode(current, current->doc, 1); - if (njs_slow_path(copy == NULL)) { - njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); + if (njs_slow_path(njs_xml_tree_has_namespaces(node))) { + njs_vm_type_error(vm, "XMLNode has namespaces"); return NJS_ERROR; } node = xmlDocCopyNode(node, current->doc, 1); if (njs_slow_path(node == NULL)) { njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); - goto error; - } - - rnode = xmlAddChild(copy, node); - if (njs_slow_path(rnode == NULL)) { - xmlFreeNode(node); - njs_vm_internal_error(vm, "xmlAddChild() failed"); - goto error; + return NJS_ERROR; } - ret = xmlReconciliateNs(current->doc, copy); - if (njs_slow_path(ret == -1)) { - njs_vm_internal_error(vm, "xmlReconciliateNs() failed"); - goto error; - } + njs_xml_list_append(current, node); njs_value_undefined_set(retval); - return njs_xml_replace_node(vm, njs_argument(args, 0), copy); - -error: - - xmlFreeNode(copy); - - return NJS_ERROR; + return NJS_OK; } @@ -864,7 +847,7 @@ njs_xml_node_ext_remove_all_attributes(njs_vm_t *vm, xmlNode *current; current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); - if (njs_slow_path(current == NULL)) { + if (njs_slow_path(!njs_xml_node_is_live(current))) { njs_vm_type_error(vm, "\"this\" is not a XMLNode object"); return NJS_ERROR; } @@ -884,12 +867,12 @@ static njs_int_t njs_xml_node_ext_remove_children(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - xmlNode *current, *copy; + xmlNode *current; njs_str_t name; njs_value_t *selector; current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0)); - if (njs_slow_path(current == NULL)) { + if (njs_slow_path(!njs_xml_node_is_live(current))) { njs_vm_type_error(vm, "\"this\" is not a XMLNode object"); return NJS_ERROR; } @@ -911,18 +894,11 @@ njs_xml_node_ext_remove_children(njs_vm_t *vm, njs_value_t *args, /* all. */ - copy = xmlDocCopyNode(current, current->doc, 1); - if (njs_slow_path(copy == NULL)) { - njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); - return NJS_ERROR; - } - - if (copy->children != NULL) { - xmlFreeNodeList(copy->children); - copy->children = NULL; + if (current->children == NULL) { + return NJS_OK; } - return njs_xml_replace_node(vm, njs_argument(args, 0), copy); + return njs_xml_list_retire(vm, current, NULL); } @@ -979,7 +955,12 @@ njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_str_t name; current = njs_vm_external(vm, njs_xml_node_proto_id, value); - if (njs_slow_path(current == NULL || current->children == NULL)) { + if (njs_slow_path(current == NULL)) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + if (retval != NULL && current->children == NULL && setval == NULL) { njs_value_undefined_set(retval); return NJS_DECLINED; } @@ -995,8 +976,7 @@ static njs_int_t njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *unused, uint32_t unused1, njs_value_t *value, njs_value_t *setval, njs_value_t *retval) { - u_char *text; - xmlNode *current, *copy, *node; + xmlNode *current, *text; njs_int_t ret; njs_str_t content; @@ -1007,14 +987,20 @@ njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *unused, uint32_t unused1, } if (retval != NULL && setval == NULL) { - text = xmlNodeGetContent(current); - ret = njs_vm_value_string_create(vm, retval, text, njs_strlen(text)); + content.start = xmlNodeGetContent(current); + ret = njs_vm_value_string_create(vm, retval, content.start, + njs_strlen(content.start)); - xmlFree(text); + xmlFree(content.start); return ret; } + if (njs_slow_path(!njs_xml_node_is_live(current))) { + njs_vm_type_error(vm, "XMLNode is detached"); + return NJS_ERROR; + } + /* set or delete. */ content.start = NULL; @@ -1036,37 +1022,32 @@ njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *unused, uint32_t unused1, } } - copy = xmlDocCopyNode(current, current->doc, 1); - if (njs_slow_path(copy == NULL)) { - njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); - return NJS_ERROR; - } + text = NULL; - node = NULL; if (retval != NULL && setval != NULL) { - node = xmlNewDocTextLen(current->doc, content.start, content.length); - if (njs_slow_path(node == NULL)) { - xmlFreeNode(copy); + text = xmlNewDocTextLen(current->doc, content.start, content.length); + if (njs_slow_path(text == NULL)) { njs_vm_internal_error(vm, "xmlNewDocTextLen() failed"); return NJS_ERROR; } } - xmlFreeNodeList(copy->children); - copy->children = node; - copy->last = node; + if (current->children != NULL) { + if (njs_slow_path(njs_xml_list_retire(vm, current, NULL) != NJS_OK)) { + xmlFreeNode(text); + return NJS_ERROR; + } + } - if (node != NULL) { - node->parent = copy; - node->prev = NULL; - node->next = NULL; + if (text != NULL) { + njs_xml_list_append(current, text); } if (retval != NULL) { njs_value_undefined_set(retval); } - return njs_xml_replace_node(vm, value, copy); + return NJS_OK; } @@ -1076,8 +1057,9 @@ njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, { size_t size; njs_int_t ret; + njs_str_t str; xmlAttr *attr; - const u_char *content, *value; + const u_char *content, *name_c, *value; u_char name_buf[512], value_buf[1024]; if (retval != NULL && setval == NULL) { @@ -1114,12 +1096,18 @@ njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, /* set or delete. */ - ret = njs_xml_str_to_c_string(vm, name, &name_buf[0], sizeof(name_buf)); - if (njs_slow_path(ret != NJS_OK)) { + if (njs_slow_path(!njs_xml_node_is_live(current))) { + njs_vm_type_error(vm, "XMLNode is detached"); + return NJS_ERROR; + } + + name_c = njs_xml_string_to_c_string(vm, name, &name_buf[0], + sizeof(name_buf)); + if (njs_slow_path(name_c == NULL)) { return NJS_ERROR; } - ret = xmlValidateQName(&name_buf[0], 0); + ret = xmlValidateQName(name_c, 0); if (njs_slow_path(ret != 0)) { njs_vm_type_error(vm, "attribute name \"%V\" is not valid", name); return NJS_ERROR; @@ -1130,7 +1118,7 @@ njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, { /* delete. */ - attr = xmlHasProp(current, &name_buf[0]); + attr = xmlHasProp(current, name_c); if (attr != NULL) { xmlRemoveProp(attr); @@ -1139,13 +1127,20 @@ njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, return NJS_OK; } - value = njs_xml_value_to_c_string(vm, setval, &value_buf[0], - sizeof(value_buf)); + if (njs_slow_path(!njs_value_is_string(setval))) { + njs_vm_type_error(vm, "setval is not a string"); + return NJS_ERROR; + } + + njs_value_string_get(vm, setval, &str); + + value = njs_xml_string_to_c_string(vm, &str, &value_buf[0], + sizeof(value_buf)); if (njs_slow_path(value == NULL)) { return NJS_ERROR; } - attr = xmlSetProp(current, &name_buf[0], value); + attr = xmlSetProp(current, name_c, value); if (njs_slow_path(attr == NULL)) { njs_vm_internal_error(vm, "xmlSetProp() failed"); return NJS_ERROR; @@ -1160,37 +1155,29 @@ njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name, static njs_int_t njs_xml_node_tag_remove(njs_vm_t *vm, njs_value_t *value, njs_str_t *name) { - size_t size; - xmlNode *current, *node, *next, *copy; + xmlNode *current, *node; current = njs_vm_external(vm, njs_xml_node_proto_id, value); - copy = xmlDocCopyNode(current, current->doc, 1); - if (njs_slow_path(copy == NULL)) { - njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); + if (njs_slow_path(!njs_xml_node_is_live(current))) { + njs_vm_type_error(vm, "XMLNode is detached"); return NJS_ERROR; } - for (node = copy->children; node != NULL; node = next) { - next = node->next; - - if (node->type != XML_ELEMENT_NODE) { - continue; - } - - size = njs_strlen(node->name); - - if (name->length != size - || njs_strncmp(name->start, node->name, size) != 0) + for (node = current->children; node != NULL; node = node->next) { + if (node->type == XML_ELEMENT_NODE + && name->length == njs_strlen(node->name) + && njs_strncmp(name->start, node->name, name->length) == 0) { - continue; + break; } + } - xmlUnlinkNode(node); - xmlFreeNode(node); + if (node == NULL) { + return NJS_OK; } - return njs_xml_replace_node(vm, value, copy); + return njs_xml_list_retire(vm, current, name); } @@ -1246,15 +1233,18 @@ static njs_int_t njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, njs_value_t *setval, njs_value_t *retval) { - size_t size; - int64_t i, length; - xmlNode *current, *node, *rnode, *copy; - njs_int_t ret; - njs_value_t *push; + size_t size; + xmlNode *current, *first, *last, *node; + int64_t i, length; + njs_int_t ret; + njs_value_t *push; njs_opaque_value_t *start; current = njs_vm_external(vm, njs_xml_node_proto_id, value); + first = NULL; + last = NULL; + if (retval != NULL && setval == NULL) { /* get. */ @@ -1302,16 +1292,22 @@ njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, /* set or delete. */ - copy = xmlDocCopyNode(current, current->doc, - 2 /* copy properties and namespaces */); - if (njs_slow_path(copy == NULL)) { - njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); - return NJS_ERROR; - } - if (retval == NULL) { /* delete. */ - return njs_xml_replace_node(vm, value, copy); + if (!njs_xml_node_is_live(current)) { + njs_vm_type_error(vm, "XMLNode is detached"); + return NJS_ERROR; + } + + if (current->children == NULL) { + return NJS_OK; + } + + if (njs_slow_path(njs_xml_list_retire(vm, current, NULL) != NJS_OK)) { + return NJS_ERROR; + } + + return NJS_OK; } if (!njs_value_is_array(setval)) { @@ -1319,6 +1315,11 @@ njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, goto error; } + if (njs_slow_path(!njs_xml_node_is_live(current))) { + njs_vm_type_error(vm, "XMLNode is detached"); + return NJS_ERROR; + } + start = (njs_opaque_value_t *) njs_vm_array_start(vm, setval); if (njs_slow_path(start == NULL)) { goto error; @@ -1333,33 +1334,51 @@ njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, goto error; } + if (njs_slow_path(njs_xml_tree_has_namespaces(node))) { + njs_vm_type_error(vm, "setval[%D] has namespaces", i); + goto error; + } + node = xmlDocCopyNode(node, current->doc, 1); if (njs_slow_path(node == NULL)) { njs_vm_internal_error(vm, "xmlDocCopyNode() failed"); goto error; } - rnode = xmlAddChild(copy, node); - if (njs_slow_path(rnode == NULL)) { - njs_vm_internal_error(vm, "xmlAddChild() failed"); - xmlFreeNode(node); + node->parent = NULL; + node->prev = last; + node->next = NULL; + + if (last != NULL) { + last->next = node; + + } else { + first = node; + } + + last = node; + } + + if (current->children != NULL) { + if (njs_slow_path(njs_xml_list_retire(vm, current, NULL) != NJS_OK)) { goto error; } } - ret = xmlReconciliateNs(current->doc, copy); - if (njs_slow_path(ret == -1)) { - njs_vm_internal_error(vm, "xmlReconciliateNs() failed"); - goto error; + current->children = first; + current->last = last; + + for (node = first; node != NULL; node = node->next) { + node->parent = current; } njs_value_undefined_set(retval); - return njs_xml_replace_node(vm, value, copy); + return NJS_OK; error: - xmlFreeNode(copy); + xmlFreeNodeList(first); return NJS_ERROR; } @@ -1390,76 +1409,115 @@ njs_xml_external_node(njs_vm_t *vm, njs_value_t *value) } -static njs_int_t -njs_xml_str_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst, +static const u_char * +njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst, size_t size) { u_char *p; - if (njs_slow_path(str->length > size - njs_length("\0"))) { - njs_vm_internal_error(vm, "njs_xml_str_to_c_string() very long string, " - "length >= %uz", size - njs_length("\0")); - return NJS_ERROR; + if (str->length >= size) { + dst = njs_mp_alloc(njs_vm_memory_pool(vm), str->length + 1); + if (njs_slow_path(dst == NULL)) { + njs_vm_memory_error(vm); + return NULL; + } } p = njs_cpymem(dst, str->start, str->length); *p = '\0'; - return NJS_OK; + return dst; } -static const u_char * -njs_xml_value_to_c_string(njs_vm_t *vm, njs_value_t *value, u_char *dst, - size_t size) +static njs_bool_t +njs_xml_node_is_live(xmlNode *node) { - u_char *p; - njs_str_t str; - njs_int_t ret; + xmlNode *parent; - ret = njs_vm_value_to_bytes(vm, &str, value); - if (njs_slow_path(ret != NJS_OK)) { - return NULL; + if (node == NULL || node->doc == NULL) { + return 0; } - if (njs_fast_path(str.length + njs_length("\0") < size)) { - ret = njs_xml_str_to_c_string(vm, &str, dst, size); - if (njs_slow_path(ret != NJS_OK)) { - return NULL; + for (parent = node->parent; parent != NULL; parent = parent->parent) { + if (parent == (xmlNode *) node->doc) { + return 1; } - - return dst; } - dst = njs_mp_alloc(njs_vm_memory_pool(vm), str.length + njs_length("\0")); - if (njs_slow_path(dst == NULL)) { - njs_vm_memory_error(vm); - return NULL; + return 0; +} + + +static xmlNode * +njs_xml_list_detach(xmlNode *parent, njs_str_t *name) +{ + xmlNode *node, *next, *first, *last, *keep_first, *keep_last; + + if (name == NULL) { + first = parent->children; + parent->children = NULL; + parent->last = NULL; + + for (node = first; node != NULL; node = node->next) { + node->parent = NULL; + } + + return first; } - p = njs_cpymem(dst, str.start, str.length); - *p = '\0'; + first = NULL; + last = NULL; + keep_first = NULL; + keep_last = NULL; - return dst; -} + for (node = parent->children; node != NULL; node = next) { + next = node->next; + if (node->type == XML_ELEMENT_NODE + && name->length == njs_strlen(node->name) + && njs_strncmp(name->start, node->name, name->length) == 0) + { + node->parent = NULL; + node->prev = last; + node->next = NULL; -static njs_int_t -njs_xml_replace_node(njs_vm_t *vm, njs_value_t *value, xmlNode *current) -{ - xmlNode *old; - njs_mp_cleanup_t *cln; + if (last != NULL) { + last->next = node; + + } else { + first = node; + } - old = njs_vm_external(vm, njs_xml_node_proto_id, value); + last = node; - if (current != NULL) { - old = xmlReplaceNode(old, current); + } else { + node->parent = parent; + node->prev = keep_last; + node->next = NULL; - } else { - xmlUnlinkNode(old); + if (keep_last != NULL) { + keep_last->next = node; + + } else { + keep_first = node; + } + + keep_last = node; + } } - njs_value_external_set(value, current); + parent->children = keep_first; + parent->last = keep_last; + + return first; +} + + +static njs_int_t +njs_xml_list_retire(njs_vm_t *vm, xmlNode *parent, njs_str_t *name) +{ + njs_mp_cleanup_t *cln; cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0); if (njs_slow_path(cln == NULL)) { @@ -1468,25 +1526,83 @@ njs_xml_replace_node(njs_vm_t *vm, njs_value_t *value, xmlNode *current) } cln->handler = njs_xml_node_cleanup; - cln->data = old; + cln->data = njs_xml_list_detach(parent, name); return NJS_OK; } static void -njs_xml_node_cleanup(void *data) +njs_xml_list_append(xmlNode *parent, xmlNode *node) { - xmlNode *current = data; + node->parent = parent; + node->prev = parent->last; + node->next = NULL; - xmlFreeNode(current); + if (parent->last != NULL) { + parent->last->next = node; + + } else { + parent->children = node; + } + + parent->last = node; +} + + +static njs_bool_t +njs_xml_tree_has_namespaces(xmlNode *node) +{ + xmlAttr *attr; + xmlNode *child; + + switch (node->type) { + case XML_ELEMENT_NODE: + break; + + case XML_TEXT_NODE: + case XML_CDATA_SECTION_NODE: + case XML_PI_NODE: + case XML_COMMENT_NODE: + return 0; + + default: + return 1; + } + + if (node->ns != NULL || node->nsDef != NULL) { + return 1; + } + + for (attr = node->properties; attr != NULL; attr = attr->next) { + if (attr->ns != NULL) { + return 1; + } + } + + for (child = node->children; child != NULL; child = child->next) { + if (njs_xml_tree_has_namespaces(child)) { + return 1; + } + } + + return 0; +} + + +static void +njs_xml_node_cleanup(void *data) +{ + xmlFreeNodeList(data); } static void njs_xml_doc_cleanup(void *data) { - njs_xml_doc_t *current = data; + njs_xml_doc_t *current; + + current = data; xmlFreeDoc(current->doc); xmlFreeParserCtxt(current->ctx); @@ -1687,6 +1803,11 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } + if (njs_slow_path(!njs_xml_node_is_live(current))) { + njs_vm_type_error(vm, "XMLNode is detached"); + return NJS_ERROR; + } + comments = njs_value_bool(njs_arg(args, nargs, 3)); excluding = njs_arg(args, nargs, 2); @@ -1699,6 +1820,13 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } + if (njs_slow_path(node->doc != current->doc + || !njs_xml_node_is_live(node))) + { + njs_vm_type_error(vm, "\"excluding\" is not in XMLNode tree"); + return NJS_ERROR; + } + nset = njs_xml_nset_create(vm, current->doc, current, XML_NSET_TREE_NO_COMMENTS); if (njs_slow_path(nset == NULL)) { diff --git a/external/qjs_xml_module.c b/external/qjs_xml_module.c index f7846848..ae21cedf 100644 --- a/external/qjs_xml_module.c +++ b/external/qjs_xml_module.c @@ -12,11 +12,19 @@ #include +typedef struct qjs_xml_retired_s qjs_xml_retired_t; + +struct qjs_xml_retired_s { + xmlNode *nodes; + qjs_xml_retired_t *next; +}; + + typedef struct { - xmlDoc *doc; - xmlParserCtxt *ctx; - xmlNode *free; - int ref_count; + xmlDoc *doc; + xmlParserCtxt *ctx; + qjs_xml_retired_t *retired; + int ref_count; } qjs_xml_doc_t; @@ -109,8 +117,12 @@ static qjs_xml_nset_t *qjs_xml_nset_create(JSContext *cx, xmlDoc *doc, static qjs_xml_nset_t *qjs_xml_nset_add(qjs_xml_nset_t *nset, qjs_xml_nset_t *add); static void qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset); -static void qjs_xml_replace_node(JSContext *cx, qjs_xml_node_t *node, - xmlNode *current); +static int qjs_xml_node_is_live(xmlNode *node); +static xmlNode *qjs_xml_list_detach(xmlNode *parent, njs_str_t *name); +static int qjs_xml_list_retire(JSContext *cx, qjs_xml_doc_t *doc, + xmlNode *parent, njs_str_t *name); +static void qjs_xml_list_append(xmlNode *parent, xmlNode *node); +static int qjs_xml_tree_has_namespaces(xmlNode *node); static void qjs_xml_error(JSContext *cx, qjs_xml_doc_t *current, const char *fmt, ...); @@ -259,6 +271,11 @@ qjs_xml_canonicalization(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } + if (!qjs_xml_node_is_live(node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return JS_EXCEPTION; + } + comments = JS_ToBool(cx, argv[2]); if (comments < 0) { return JS_EXCEPTION; @@ -277,6 +294,13 @@ qjs_xml_canonicalization(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } + if (nd->doc->doc != doc + || !qjs_xml_node_is_live(nd->node)) + { + JS_ThrowTypeError(cx, "\"excluding\" is not in XMLNode tree"); + return JS_EXCEPTION; + } + nset = qjs_xml_nset_create(cx, doc, node, XML_NSET_TREE_NO_COMMENTS); if (nset == NULL) { return JS_ThrowOutOfMemory(cx); @@ -504,18 +528,16 @@ qjs_xml_doc_get_own_property_names(JSContext *cx, JSPropertyEnum **ptab, static void qjs_xml_doc_free(JSRuntime *rt, qjs_xml_doc_t *current) { - xmlNode *node, *next; + qjs_xml_retired_t *retired, *next; if (--current->ref_count > 0) { return; } - node = current->free; - - while (node != NULL) { - next = node->next; - xmlFreeNode(node); - node = next; + for (retired = current->retired; retired != NULL; retired = next) { + next = retired->next; + xmlFreeNodeList(retired->nodes); + js_free_rt(rt, retired); } if (current->doc != NULL) { @@ -696,6 +718,11 @@ qjs_xml_node_attr_modify(JSContext *cx, JSValue current, const u_char *name, return -1; } + if (!qjs_xml_node_is_live(node->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return -1; + } + if (xmlValidateQName(name, 0) != 0) { JS_ThrowTypeError(cx, "attribute name \"%s\" is not valid", name); return -1; @@ -711,6 +738,11 @@ qjs_xml_node_attr_modify(JSContext *cx, JSValue current, const u_char *name, return 1; } + if (!JS_IsString(setval)) { + JS_ThrowTypeError(cx, "setval is not a string"); + return -1; + } + value = (const u_char *) JS_ToCString(cx, setval); if (value == NULL) { return -1; @@ -731,8 +763,7 @@ static int qjs_xml_node_tag_modify(JSContext *cx, JSValue obj, njs_str_t *name, JSValue setval) { - size_t size; - xmlNode *node, *next, *copy; + xmlNode *node; qjs_xml_node_t *current; current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_NODE); @@ -740,6 +771,11 @@ qjs_xml_node_tag_modify(JSContext *cx, JSValue obj, njs_str_t *name, return -1; } + if (!qjs_xml_node_is_live(current->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return -1; + } + if (!JS_IsNullOrUndefined(setval)) { JS_ThrowTypeError(cx, "XMLNode.$tag$xxx is not assignable, " "use addChild() or node.$tags = [node1, node2, ..] " @@ -747,35 +783,26 @@ qjs_xml_node_tag_modify(JSContext *cx, JSValue obj, njs_str_t *name, return -1; } - copy = xmlDocCopyNode(current->node, current->doc->doc, 1); - if (copy == NULL) { - JS_ThrowInternalError(cx, "xmlDocCopyNode() failed"); - return -1; - } - - for (node = copy->children; node != NULL; node = next) { - next = node->next; - - if (node->type != XML_ELEMENT_NODE) { - continue; + if (name->length == 0) { + if (current->node->children != NULL) { + return qjs_xml_list_retire(cx, current->doc, current->node, NULL); } - size = njs_strlen(node->name); + return 1; + } - if (name->length > 0 - && (name->length != size - || njs_strncmp(name->start, node->name, size) != 0)) + for (node = current->node->children; node != NULL; node = node->next) { + if (node->type == XML_ELEMENT_NODE + && name->length == njs_strlen(node->name) + && njs_strncmp(name->start, node->name, name->length) == 0) { - continue; + break; } - - xmlUnlinkNode(node); - - node->next = current->doc->free; - current->doc->free = node; } - qjs_xml_replace_node(cx, current, copy); + if (node != NULL) { + return qjs_xml_list_retire(cx, current->doc, current->node, name); + } return 1; } @@ -785,9 +812,9 @@ static int qjs_xml_node_tags_modify(JSContext *cx, JSValue obj, njs_str_t *name, JSValue setval) { - int32_t len, i; - xmlNode *node, *rnode, *copy; JSValue length, v; + xmlNode *first, *last, *node; + uint32_t len, i; qjs_xml_node_t *current; current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_NODE); @@ -795,6 +822,26 @@ qjs_xml_node_tags_modify(JSContext *cx, JSValue obj, njs_str_t *name, return -1; } + if (!qjs_xml_node_is_live(current->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return -1; + } + + if (name->length > 0) { + JS_ThrowTypeError(cx, "XMLNode $tags$xxx is not assignable, use " + "addChild() or node.$tags = [node1, node2, ..] " + "syntax"); + return -1; + } + + if (JS_IsNullOrUndefined(setval)) { + if (current->node->children == NULL) { + return 1; + } + + return qjs_xml_list_retire(cx, current->doc, current->node, NULL); + } + if (!qjs_is_array(cx, setval)) { JS_ThrowTypeError(cx, "setval is not an array"); return -1; @@ -805,16 +852,15 @@ qjs_xml_node_tags_modify(JSContext *cx, JSValue obj, njs_str_t *name, return -1; } - if (JS_ToInt32(cx, &len, length) < 0) { + if (JS_ToUint32(cx, &len, length) < 0) { + JS_FreeValue(cx, length); return -1; } - copy = xmlDocCopyNode(current->node, current->doc->doc, - 2 /* copy properties and namespaces */); - if (copy == NULL) { - JS_ThrowInternalError(cx, "xmlDocCopyNode() failed"); - return -1; - } + JS_FreeValue(cx, length); + + first = NULL; + last = NULL; for (i = 0; i < len; i++) { v = JS_GetPropertyUint32(cx, setval, i); @@ -828,32 +874,54 @@ qjs_xml_node_tags_modify(JSContext *cx, JSValue obj, njs_str_t *name, goto error; } + if (qjs_xml_tree_has_namespaces(node)) { + JS_ThrowTypeError(cx, "setval[%u] has namespaces", i); + goto error; + } + node = xmlDocCopyNode(node, current->doc->doc, 1); if (node == NULL) { JS_ThrowInternalError(cx, "xmlDocCopyNode() failed"); goto error; } - rnode = xmlAddChild(copy, node); - if (rnode == NULL) { - xmlFreeNode(node); - JS_ThrowInternalError(cx, "xmlAddChild() failed"); - goto error; + node->parent = NULL; + node->prev = last; + node->next = NULL; + + if (last != NULL) { + last->next = node; + + } else { + first = node; } + + last = node; } - if (xmlReconciliateNs(current->doc->doc, copy) == -1) { - JS_ThrowInternalError(cx, "xmlReconciliateNs() failed"); + if (!qjs_xml_node_is_live(current->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); goto error; } - qjs_xml_replace_node(cx, current, copy); + if (current->node->children != NULL) { + if (qjs_xml_list_retire(cx, current->doc, current->node, NULL) < 0) { + goto error; + } + } + + current->node->children = first; + current->node->last = last; + + for (node = first; node != NULL; node = node->next) { + node->parent = current->node; + } return 1; error: - xmlFreeNode(copy); + xmlFreeNodeList(first); return -1; } @@ -862,7 +930,7 @@ error: static int qjs_xml_node_text_handler(JSContext *cx, JSValue current, JSValue setval) { - xmlNode *copy, *text; + xmlNode *text; njs_str_t content; qjs_xml_node_t *node; @@ -874,7 +942,17 @@ qjs_xml_node_text_handler(JSContext *cx, JSValue current, JSValue setval) return -1; } + if (!qjs_xml_node_is_live(node->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return -1; + } + if (!JS_IsNullOrUndefined(setval)) { + if (!JS_IsString(setval)) { + JS_ThrowTypeError(cx, "setval is not a string"); + return -1; + } + content.start = (u_char *) JS_ToCStringLen(cx, &content.length, setval); if (content.start == NULL) { return -1; @@ -887,39 +965,27 @@ qjs_xml_node_text_handler(JSContext *cx, JSValue current, JSValue setval) } } - copy = xmlDocCopyNode(node->node, node->doc->doc, 1); - if (copy == NULL) { - if (!JS_IsNullOrUndefined(setval)) { - JS_FreeCString(cx, (char *) content.start); - } - - JS_ThrowInternalError(cx, "xmlDocCopyNode() failed"); - return -1; - } - text = NULL; if (!JS_IsNullOrUndefined(setval)) { text = xmlNewDocTextLen(node->doc->doc, content.start, content.length); JS_FreeCString(cx, (char *) content.start); if (text == NULL) { - xmlFreeNode(copy); JS_ThrowInternalError(cx, "xmlNewDocTextLen() failed"); return -1; } } - xmlFreeNodeList(copy->children); - copy->children = text; - copy->last = text; + if (node->node->children != NULL) { + if (qjs_xml_list_retire(cx, node->doc, node->node, NULL) < 0) { + xmlFreeNode(text); + return -1; + } + } if (text != NULL) { - text->parent = copy; - text->prev = NULL; - text->next = NULL; + qjs_xml_list_append(node->node, text); } - qjs_xml_replace_node(cx, node, copy); - return 1; } @@ -1351,7 +1417,7 @@ static JSValue qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv) { - xmlNode *copy, *node, *rnode; + xmlNode *node; qjs_xml_node_t *current; current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE); @@ -1360,44 +1426,30 @@ qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val, return JS_EXCEPTION; } + if (!qjs_xml_node_is_live(current->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return JS_EXCEPTION; + } + node = qjs_xml_node(cx, argv[0], NULL); if (node == NULL) { return JS_EXCEPTION; } - copy = xmlDocCopyNode(current->node, current->doc->doc, 1); - if (copy == NULL) { - JS_ThrowInternalError(cx, "xmlDocCopyNode() failed"); + if (qjs_xml_tree_has_namespaces(node)) { + JS_ThrowTypeError(cx, "XMLNode has namespaces"); return JS_EXCEPTION; } node = xmlDocCopyNode(node, current->doc->doc, 1); if (node == NULL) { JS_ThrowInternalError(cx, "xmlDocCopyNode() failed"); - goto error; - } - - rnode = xmlAddChild(copy, node); - if (rnode == NULL) { - xmlFreeNode(node); - JS_ThrowInternalError(cx, "xmlAddChild() failed"); - goto error; - } - - if (xmlReconciliateNs(current->doc->doc, copy) == -1) { - JS_ThrowInternalError(cx, "xmlReconciliateNs() failed"); - goto error; + return JS_EXCEPTION; } - qjs_xml_replace_node(cx, current, copy); + qjs_xml_list_append(current->node, node); return JS_UNDEFINED; - -error: - - xmlFreeNode(copy); - - return JS_EXCEPTION; } @@ -1508,6 +1560,11 @@ qjs_xml_node_remove_all_attributes(JSContext *cx, JSValueConst this_val, return JS_EXCEPTION; } + if (!qjs_xml_node_is_live(current->node)) { + JS_ThrowTypeError(cx, "XMLNode is detached"); + return JS_EXCEPTION; + } + node = current->node; if (node->properties != NULL) { @@ -1967,24 +2024,165 @@ qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset) } -static void -qjs_xml_replace_node(JSContext *cx, qjs_xml_node_t *node, xmlNode *current) +static int +qjs_xml_node_is_live(xmlNode *node) +{ + xmlNode *parent; + + if (node == NULL || node->doc == NULL) { + return 0; + } + + for (parent = node->parent; parent != NULL; parent = parent->parent) { + if (parent == (xmlNode *) node->doc) { + return 1; + } + } + + return 0; +} + + +static xmlNode * +qjs_xml_list_detach(xmlNode *parent, njs_str_t *name) +{ + xmlNode *node, *next, *first, *last, *keep_first, *keep_last; + + if (name == NULL) { + first = parent->children; + parent->children = NULL; + parent->last = NULL; + + for (node = first; node != NULL; node = node->next) { + node->parent = NULL; + } + + return first; + } + + first = NULL; + last = NULL; + keep_first = NULL; + keep_last = NULL; + + for (node = parent->children; node != NULL; node = next) { + next = node->next; + + if (node->type == XML_ELEMENT_NODE + && name->length == njs_strlen(node->name) + && njs_strncmp(name->start, node->name, name->length) == 0) + { + node->parent = NULL; + node->prev = last; + node->next = NULL; + + if (last != NULL) { + last->next = node; + + } else { + first = node; + } + + last = node; + + } else { + node->parent = parent; + node->prev = keep_last; + node->next = NULL; + + if (keep_last != NULL) { + keep_last->next = node; + + } else { + keep_first = node; + } + + keep_last = node; + } + } + + parent->children = keep_first; + parent->last = keep_last; + + return first; +} + + +static int +qjs_xml_list_retire(JSContext *cx, qjs_xml_doc_t *doc, xmlNode *parent, + njs_str_t *name) { - xmlNode *old; + qjs_xml_retired_t *retired; + + retired = js_malloc(cx, sizeof(qjs_xml_retired_t)); + if (retired == NULL) { + JS_ThrowOutOfMemory(cx); + return -1; + } + + retired->nodes = qjs_xml_list_detach(parent, name); + retired->next = doc->retired; + doc->retired = retired; + + return 1; +} - old = node->node; - if (current != NULL) { - old = xmlReplaceNode(old, current); +static void +qjs_xml_list_append(xmlNode *parent, xmlNode *node) +{ + node->parent = parent; + node->prev = parent->last; + node->next = NULL; + + if (parent->last != NULL) { + parent->last->next = node; } else { - xmlUnlinkNode(old); + parent->children = node; } - node->node = current; + parent->last = node; +} + - old->next = node->doc->free; - node->doc->free = old; +static int +qjs_xml_tree_has_namespaces(xmlNode *node) +{ + xmlAttr *attr; + xmlNode *child; + + switch (node->type) { + case XML_ELEMENT_NODE: + break; + + case XML_TEXT_NODE: + case XML_CDATA_SECTION_NODE: + case XML_PI_NODE: + case XML_COMMENT_NODE: + return 0; + + default: + return 1; + } + + if (node->ns != NULL || node->nsDef != NULL) { + return 1; + } + + for (attr = node->properties; attr != NULL; attr = attr->next) { + if (attr->ns != NULL) { + return 1; + } + } + + for (child = node->children; child != NULL; child = child->next) { + if (qjs_xml_tree_has_namespaces(child)) { + return 1; + } + } + + return 0; } diff --git a/test/xml/xml.t.mjs b/test/xml/xml.t.mjs index 015f7292..db52f0ff 100644 --- a/test/xml/xml.t.mjs +++ b/test/xml/xml.t.mjs @@ -5,6 +5,10 @@ flags: [async] import xml from 'xml'; +function has_quickjs() { + return (typeof njs != 'undefined' && njs.engine == 'QuickJS'); +} + let parse_tsuite = { name: "parse()", skip: () => (!has_njs()), @@ -164,6 +168,19 @@ let c14n_tsuite = { { doc: `ToveJani`, call: (doc) => xml.exclusiveC14n(doc.note, doc.note.to), expected: `Jani` }, + { doc: `A`, + call: (doc) => { + let other = xml.parse(``); + return xml.exclusiveC14n(doc.r, other.other); + }, + exception: 'TypeError: "excluding" is not in XMLNode tree' }, + { doc: `A`, + call: (doc) => { + let a = doc.r.a; + doc.r.removeChildren('a'); + return xml.exclusiveC14n(doc.r, a); + }, + exception: 'TypeError: "excluding" is not in XMLNode tree' }, { doc: ``, call: (doc) => xml.exclusiveC14n(doc, 1), exception: 'TypeError: "excluding" argument is not a XMLNode object' }, @@ -212,6 +229,73 @@ let modify_tsuite = { return doc.note.$text; }, expected: 'WAKA' }, + { get: () => { + let mutations = [ + (node) => node.setText('first'), + (node) => node.removeText(), + (node) => node.removeChildren(), + (node) => node.removeChildren('a'), + (node, doc) => node.addChild(doc.r.b), + (node) => { delete node.$tag$a; }, + (node, doc) => { node.$tags = [doc.r.a]; }, + (node) => { delete node.$tags; }, + ]; + let passed = 0; + + for (let i = 0; i < mutations.length; i++) { + for (let j = 0; j < 2; j++) { + let current = xml.parse(`AB`); + let first = current.r; + let second = current.r; + let mutate = mutations[i]; + let reverse = (j != 0); + let target = reverse ? second : first; + let alias = reverse ? first : second; + + mutate(target, current); + alias.setText('final'); + + if (first.$text != 'final' + || second.$text != 'final' + || current.r.$text != 'final') + { + return false; + } + + passed++; + } + } + + return passed; + }, + expected: 16 }, + { get: (doc) => { + let direct = doc.note; + let root = doc.$root; + let parent = doc.note.to.$parent; + let tag = doc.note.$tag$to.$parent; + let tags = doc.note.$tags$to[0].$parent; + + direct.setText('direct'); + root.setText('root'); + parent.setText('parent'); + tag.setText('tag'); + tags.setText('tags'); + + return [direct.$text, root.$text, parent.$text, tag.$text, + tags.$text, doc.note.$text].toString(); + }, + expected: 'tags,tags,tags,tags,tags,tags' }, + { get: (doc) => { + let to = doc.note.to; + let attrs = to.$attrs; + + to.setText('changed'); + to.setAttribute('a', 'updated'); + + return [attrs.a, to.$text, doc.note.to.$text].toString(); + }, + expected: 'updated,changed,changed' }, { get: (doc) => { doc.note.setText('WAKA'); return xml.serializeToString(doc); @@ -262,6 +346,24 @@ let modify_tsuite = { return 'missing exception'; }, expected: 'true,ToveJani' }, + { get: (doc) => { + let called = false; + let value = {toString: () => { + called = true; + doc.note.removeChildren(); + return 'changed'; + }}; + + try { + doc.note.setText(value); + } catch (e) { + return [called, e instanceof TypeError, + xml.serializeToString(doc)].toString(); + } + + return 'missing exception'; + }, + expected: 'false,true,ToveJani' }, { get: (doc) => { doc.note.setText(null); return doc.note.$text; @@ -290,15 +392,9 @@ let modify_tsuite = { { get: (doc) => { let to = doc.note.to; doc.$root.$text = ''; - return to.$name; - }, - expected: 'to' }, - { get: (doc) => { - let to = doc.note.to; - doc.$root.$text = ''; - return [to.$name, to.$text, to.$attr$b, to.$parent.$name].toString(); + return [to.$name, to.$text, to.$attr$b, to.$parent].toString(); }, - expected: 'to,Tove,bar,note' }, + expected: 'to,Tove,bar,' }, { get: (doc) => { doc.note.to.setAttribute('aaa', 'foo'); doc.note.to.setAttribute('bbb', 'Tove` }, + { get: (doc) => { + let value = 'x'.repeat(2048); + doc.note.to.setAttribute('aaa', value); + doc.note.to.$attr$bbb = value; + return [doc.note.to.$attr$aaa.length, + doc.note.to.$attr$bbb.length].toString(); + }, + expected: '2048,2048' }, + { get: (doc) => { + let called = false; + let value = {toString: () => { + called = true; + doc.note.removeChildren('to'); + return 'changed'; + }}; + + try { + doc.note.to.setAttribute('aaa', value); + } catch (e) { + return [called, e instanceof TypeError, + xml.serializeToString(doc)].toString(); + } + + return 'missing exception'; + }, + expected: 'false,true,ToveJani' }, { get: (doc) => { doc.note.to.removeAttribute('a'); return xml.serializeToString(doc.note.to); @@ -365,22 +487,29 @@ let modify_tsuite = { doc.note.addChild(doc2); return xml.serializeToString(doc); }, - expected: `ToveJani` }, + exception: 'TypeError: XMLNode has namespaces' }, { get: (doc) => { - let doc2 = xml.parse(``); + let doc2 = xml.parse(`value`); doc.note.addChild(doc2); doc.note.addChild(doc2); return xml.serializeToString(doc); }, - expected: `ToveJani` }, + expected: 'ToveJani' + + 'valuevalue' }, { get: (doc) => { - doc.note.removeChildren('to'); - return xml.serializeToString(doc); + let source = xml.parse(`]>&e;`); + + try { + doc.note.addChild(source.root); + } catch (e) { + return e instanceof TypeError; + } + + return false; }, - expected: `Jani` }, + expected: true }, { get: (doc) => { - let note = doc.note; - note.removeChildren('to'); + doc.note.removeChildren('to'); return xml.serializeToString(doc); }, expected: `Jani` }, @@ -416,12 +545,16 @@ let modify_tsuite = { throw Error('unexpected exception'); }, expected: 'OK' }, - { doc: `ABC`, + { doc: `ABCD`, get: (doc) => { + let b = doc.root.b; + let c = doc.root.c; doc.$root.removeChildren('a'); + b.setText('changed'); + c.setAttribute('x', 'y'); return xml.serializeToString(doc); }, - expected: `B` }, + expected: 'changedC' }, { doc: `ABC`, get: (doc) => { doc.$root.removeChildren(); @@ -440,6 +573,77 @@ let modify_tsuite = { return xml.serializeToString(doc); }, expected: `` }, + { doc: ``, + get: (doc) => { + delete doc.root.$tags; + return xml.serializeToString(doc); + }, + expected: `` }, + { doc: `A`, + get: (doc) => { + delete doc.root.$tags; + return xml.serializeToString(doc); + }, + expected: `` }, + { doc: ``, + get: (doc) => { + let source = xml.parse(`value`); + doc.root.$tags = [source]; + return xml.serializeToString(doc); + }, + expected: `value` }, + { doc: `value`, + skip: () => !has_quickjs(), + get: (doc) => { + let source = xml.parse(``); + let target = doc.root.target; + let values = []; + + Object.defineProperty(values, '0', { + get: () => { + doc.root.removeChildren('target'); + return source; + }, + }); + Object.defineProperty(values, 'length', {value: 1}); + + try { + target.$tags = values; + } catch (e) { + return [e instanceof TypeError, target.$parent, + xml.serializeToString(doc), + target.$name, target.$text].toString(); + } + + return 'missing exception'; + }, + expected: 'true,,,target,value' }, + { doc: `AB`, + get: (doc) => { + let source = xml.parse(``); + + try { + doc.root.$tags$a = [source]; + } catch (e) { + return [e instanceof TypeError, xml.serializeToString(doc)].toString(); + } + + return 'missing exception'; + }, + expected: 'true,AB' }, + { doc: `A`, + get: (doc) => { + let values = new Array(2147483648); + + try { + doc.root.$tags = values; + } catch (e) { + return xml.serializeToString(doc); + } + + return 'missing exception'; + }, + expected: 'A' }, { doc: `ABC`, get: (doc) => { delete doc.$root.$tag$a; @@ -447,22 +651,130 @@ let modify_tsuite = { }, expected: `B` }, { get: (doc) => { - doc.note.$tags = [doc.note.to]; + doc.note.$tags = [doc.note.to, doc.note.from]; return xml.serializeToString(doc); }, - expected: `Tove` }, + expected: 'Tove' + + 'Jani' }, { get: (doc) => { let doc2 = xml.parse(``); doc.note.$tags = [doc.note.to, doc2]; return xml.serializeToString(doc); }, - expected: `Tove` }, + exception: 'TypeError: setval[1] has namespaces' }, { get: (doc) => { let doc2 = xml.parse(``); doc.note.$tags = [doc2, doc.note.to]; return xml.serializeToString(doc); }, - expected: `Tove` }, + exception: 'TypeError: setval[0] has namespaces' }, + { get: () => { + let mutations = [ + (node) => node.addChild(xml.parse(``)), + (node) => node.removeChildren(), + (node) => node.removeChildren('child'), + (node) => { delete node.$tag$child; }, + (node) => { node.$tags = []; }, + (node) => { delete node.$tags; }, + ]; + let passed = 0; + + for (let i = 0; i < mutations.length; i++) { + let current = xml.parse( + `C`); + let target = current.r.target; + let mutate = mutations[i]; + current.r.removeChildren('target'); + + try { + mutate(target); + + } catch (e) { + if (e instanceof TypeError + && target.$parent === undefined + && xml.serializeToString(current) == '' + && target.$name == 'target' + && target.$text == 'C') + { + passed++; + continue; + } + } + + return false; + } + + return passed; + }, + expected: 6 }, + { doc: `B`, + get: (doc) => { + let a = doc.r.a; + let b = a.b; + doc.r.removeChildren('a'); + + try { + b.setText('changed'); + } catch (e) { + return [a.$parent, b.$parent.$name, b.$text, + e instanceof TypeError].toString(); + } + + return 'missing exception'; + }, + expected: ',a,B,true' }, + { get: (doc) => { + let to = doc.note.to; + doc.note.removeChildren('to'); + + try { + to.setText('changed'); + } catch (e) { + return [to.$parent, to.$text, e instanceof TypeError].toString(); + } + + return 'missing exception'; + }, + expected: ',Tove,true' }, + { get: (doc) => { + let to = doc.note.to; + doc.note.removeChildren('to'); + + try { + to.setAttribute('x', 'y'); + } catch (e) { + return e instanceof TypeError; + } + + return false; + }, + expected: true }, + { get: (doc) => { + let to = doc.note.to; + doc.note.removeChildren('to'); + + try { + to.removeAllAttributes(); + } catch (e) { + return e instanceof TypeError; + } + + return false; + }, + expected: true }, + { get: (doc) => { + let to = doc.note.to; + doc.note.removeChildren('to'); + + try { + xml.exclusiveC14n(to); + } catch (e) { + return e instanceof TypeError; + } + + return false; + }, + expected: true }, ]}; run([ -- 2.47.3