]> git.kaiwu.me - njs.git/commitdiff
XML: mutate XML nodes in place
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 10 Sep 2026 03:26:26 +0000 (20:26 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Fri, 11 Sep 2026 01:29:10 +0000 (18:29 -0700)
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
external/qjs_xml_module.c
test/xml/xml.t.mjs

index f37f887991d963991ddc28f0a050da51816d5f26..08979becb40acb36c8ca6e29ea0eb406da773fb5 100644 (file)
@@ -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)) {
index f78468488b3ab8b88cc8f1f437356eb3ff74b8ae..ae21cedfe4f0582f97357ef269d18e379504a49d 100644 (file)
 #include <libxml/xpathInternals.h>
 
 
+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;
 }
 
 
index 015f7292e31e8a07b64966b189546382f7af0d16..db52f0ff5371ab2e9d0985cd503862308fa76cde 100644 (file)
@@ -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: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>`,
           call: (doc) => xml.exclusiveC14n(doc.note, doc.note.to),
           expected: `<note><from>Jani</from></note>` },
+        { doc: `<r><a>A</a></r>`,
+          call: (doc) => {
+              let other = xml.parse(`<other></other>`);
+              return xml.exclusiveC14n(doc.r, other.other);
+          },
+          exception: 'TypeError: "excluding" is not in XMLNode tree' },
+        { doc: `<r><a>A</a></r>`,
+          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: `<r></r>`,
           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(`<r><a>A</a><b>B</b></r>`);
+                    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,<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>' },
         { 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', '<bar\"');
@@ -322,6 +418,32 @@ let modify_tsuite = {
             return xml.serializeToString(doc.note.to);
           },
           expected: `<to a="foo" aaa="foo2" b="bar">Tove</to>` },
+        { 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,<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>' },
         { 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: `<note xmlns:n0="http://a"><to a="foo" b="bar">Tove</to><from>Jani</from><n0:pdu></n0:pdu></note>` },
+          exception: 'TypeError: XMLNode has namespaces' },
         { get: (doc) => {
-            let doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);
+            let doc2 = xml.parse(`<child>value</child>`);
             doc.note.addChild(doc2);
             doc.note.addChild(doc2);
             return xml.serializeToString(doc);
             },
-          expected: `<note xmlns:n0="http://a"><to a="foo" b="bar">Tove</to><from>Jani</from><n0:pdu></n0:pdu><n0:pdu></n0:pdu></note>` },
+          expected: '<note><to a="foo" b="bar">Tove</to><from>Jani</from>'
+                    + '<child>value</child><child>value</child></note>' },
         { get: (doc) => {
-            doc.note.removeChildren('to');
-            return xml.serializeToString(doc);
+            let source = xml.parse(`<!DOCTYPE root [<!ENTITY e "text">]><root>&e;</root>`);
+
+            try {
+                doc.note.addChild(source.root);
+            } catch (e) {
+                return e instanceof TypeError;
+            }
+
+            return false;
           },
-          expected: `<note><from>Jani</from></note>` },
+          expected: true },
         { get: (doc) => {
-            let note = doc.note;
-            note.removeChildren('to');
+            doc.note.removeChildren('to');
             return xml.serializeToString(doc);
           },
           expected: `<note><from>Jani</from></note>` },
@@ -416,12 +545,16 @@ let modify_tsuite = {
             throw Error('unexpected exception');
           },
           expected: 'OK' },
-        { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
+        { doc: `<root><a>A</a><b>B</b><c>C</c><a>D</a></root>`,
           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: `<root><b>B</b></root>` },
+          expected: '<root><b>changed</b><c x="y">C</c></root>' },
         { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
           get: (doc) => {
             doc.$root.removeChildren();
@@ -440,6 +573,77 @@ let modify_tsuite = {
             return xml.serializeToString(doc);
           },
           expected: `<root></root>` },
+        { doc: `<root></root>`,
+          get: (doc) => {
+            delete doc.root.$tags;
+            return xml.serializeToString(doc);
+          },
+          expected: `<root></root>` },
+        { doc: `<root><a>A</a></root>`,
+          get: (doc) => {
+            delete doc.root.$tags;
+            return xml.serializeToString(doc);
+          },
+          expected: `<root></root>` },
+        { doc: `<root></root>`,
+          get: (doc) => {
+            let source = xml.parse(`<child>value</child>`);
+            doc.root.$tags = [source];
+            return xml.serializeToString(doc);
+          },
+          expected: `<root><child>value</child></root>` },
+        { doc: `<root><target><old>value</old></target></root>`,
+          skip: () => !has_quickjs(),
+          get: (doc) => {
+            let source = xml.parse(`<new></new>`);
+            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,,<root></root>,target,value' },
+        { doc: `<root><a>A</a><b>B</b></root>`,
+          get: (doc) => {
+            let source = xml.parse(`<child/>`);
+
+            try {
+                doc.root.$tags$a = [source];
+            } catch (e) {
+                return [e instanceof TypeError, xml.serializeToString(doc)].toString();
+            }
+
+            return 'missing exception';
+          },
+          expected: 'true,<root><a>A</a><b>B</b></root>' },
+        { doc: `<root><a>A</a></root>`,
+          get: (doc) => {
+            let values = new Array(2147483648);
+
+            try {
+                doc.root.$tags = values;
+            } catch (e) {
+                return xml.serializeToString(doc);
+            }
+
+            return 'missing exception';
+          },
+          expected: '<root><a>A</a></root>' },
         { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
           get: (doc) => {
             delete doc.$root.$tag$a;
@@ -447,22 +651,130 @@ let modify_tsuite = {
           },
           expected: `<root><b>B</b></root>` },
         { get: (doc) => {
-            doc.note.$tags = [doc.note.to];
+            doc.note.$tags = [doc.note.to, doc.note.from];
             return xml.serializeToString(doc);
             },
-          expected: `<note><to a="foo" b="bar">Tove</to></note>` },
+          expected: '<note><to a="foo" b="bar">Tove</to>'
+                    + '<from>Jani</from></note>' },
         { get: (doc) => {
             let doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);
             doc.note.$tags = [doc.note.to, doc2];
             return xml.serializeToString(doc);
             },
-          expected: `<note xmlns:n0="http://a"><to a="foo" b="bar">Tove</to><n0:pdu></n0:pdu></note>` },
+          exception: 'TypeError: setval[1] has namespaces' },
         { get: (doc) => {
             let doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);
             doc.note.$tags = [doc2, doc.note.to];
             return xml.serializeToString(doc);
             },
-          expected: `<note xmlns:n0="http://a"><n0:pdu></n0:pdu><to a="foo" b="bar">Tove</to></note>` },
+          exception: 'TypeError: setval[0] has namespaces' },
+        { get: () => {
+            let mutations = [
+                (node) => node.addChild(xml.parse(`<new></new>`)),
+                (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(
+                    `<r><target><child>C</child></target></r>`);
+                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) == '<r></r>'
+                        && target.$name == 'target'
+                        && target.$text == 'C')
+                    {
+                        passed++;
+                        continue;
+                    }
+                }
+
+                return false;
+            }
+
+            return passed;
+          },
+          expected: 6 },
+        { doc: `<r><a><b>B</b></a></r>`,
+          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([