]> git.kaiwu.me - njs.git/commitdiff
XML: added XMLNode API to modify XML documents.
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 23 Feb 2023 03:13:08 +0000 (19:13 -0800)
committerDmitry Volyntsev <xeioex@nginx.com>
Thu, 23 Feb 2023 03:13:08 +0000 (19:13 -0800)
    - delete node.$attr$a is a shorthand syntax for
        node.removeAttribute('a')

    - node.removeAllAttributes() removes all attributes of the node.

    - node.removeAttribute(attr_name) removes attribute named
        attr_name.

    - node.setAttribute(attr_name, value || null) sets a value for an
    attr_name. When value is null attribute named attr_name is
    deleted.

    - node.$attr$a = 'xxx' is a shorthand syntax for
        node.setAttribute('a', 'xxx');

    - The methods and operations below make copy-on-write
        changes to the original XML parsed document.

        For example:
        var doc = xml.parse(<r><a><b/></a></r>);
        var b = doc.$root.a.b;
        doc.$root.removeAllChildren();

        console.log((new TextDecoder()).decode(xml.c14n(doc))); /* <r></r> */
        console.log(b); /* XMLNode {$name:'b'} */
        console.log(b.$parent); /* XMLNode {$name:'a',$tags:[XMLNode
                                  {$name:'b'}] */

        "b" is valid after removeAllChildren() call, but is not a part
        of the document tree anymore.

    - node.addChild(nd) adds XMLNode as a child to node.
      nd recursively copied before adding to the node.

    - node.removeChildren(tag_name?) removes all the children tags
        named tag_name. If tag_name is absent all children tags are
        removed.

    - node.removeText() removes the node's text value.

    - node.setText(string || null) sets a text value for the node.
        When value is null the node's text is deleted.

    - node.$tags = [node1, node2, ..] is a shorthand syntax for
        node.removeChildren();
        node.addChild(node1);
        node.addChild(node2)

    - node.$text = 'xxx' is a shorthand syntax for
        node.setText('xxx');

    In addition the following method were added:
    - xml.serialize() is the same as xml.c14n()

    - xml.serializeToString() is the same as xml.c14n()
        except it returns the result as string.

    Example:
        const xml = require("xml");
        let data = `<note><to b="bar" a="foo">Tove</to><from>Jani</from></note>`;
        let doc = xml.parse(data);

        doc.$root.to.$attr$b = 'bar2';
        doc.$root.to.setAttribute('c', 'baz');
        delete doc.$root.to.$attr$a;

        console.log(xml.serializeToString(doc.$root.to)) /* '<to b="bar2" c="baz">Tove</to>' */

        doc.$root.to.removeAllAttributes();
        doc.$root.from.$text = 'Jani2';

        console.log(xml.serializeToString(doc)) /* '<note><to>Tove</to><from>Jani2</from></note>' */

        doc.$root.to.$tags = [xml.parse(`<a/>`), xml.parse(`<b/>`)];
        doc.$root.to.addChild(xml.parse(`<a/>`));

        console.log(xml.serializeToString(doc.$root.to)) /* '<to><a></a><b></b><a></a></to>' */

        doc.$root.to.removeChildren('a');

        console.log(xml.serializeToString(doc.$root.to)) /* '<to><b></b></to>' */

external/njs_xml_module.c
src/test/njs_unit_test.c
test/xml/saml_verify.t.js
ts/njs_modules/xml.d.ts

index b0bae907ef555af7048ac99279c861b6e100f40b..b8203cce5d2919811e58c11f138141ae60cb5719 100644 (file)
@@ -39,12 +39,11 @@ struct njs_xml_nset_s {
 static njs_int_t njs_xml_ext_parse(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused);
 static njs_int_t njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args,
-    njs_uint_t nargs, njs_index_t unused);
+    njs_uint_t nargs, njs_index_t magic);
 static njs_int_t njs_xml_doc_ext_prop_keys(njs_vm_t *vm, njs_value_t *value,
     njs_value_t *keys);
 static njs_int_t njs_xml_doc_ext_root(njs_vm_t *vm, njs_object_prop_t *prop,
      njs_value_t *value, njs_value_t *unused, njs_value_t *retval);
-static void njs_xml_doc_cleanup(void *data);
 static njs_int_t njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value,
     njs_value_t *keys);
 static njs_int_t njs_xml_node_ext_prop_handler(njs_vm_t *vm,
@@ -55,6 +54,8 @@ static njs_int_t njs_xml_attr_ext_prop_keys(njs_vm_t *vm, njs_value_t *value,
 static njs_int_t njs_xml_attr_ext_prop_handler(njs_vm_t *vm,
     njs_object_prop_t *prop, njs_value_t *value, njs_value_t *unused,
     njs_value_t *retval);
+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);
 static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 static njs_int_t njs_xml_node_ext_name(njs_vm_t *vm, njs_object_prop_t *prop,
@@ -63,11 +64,44 @@ static njs_int_t njs_xml_node_ext_ns(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 static njs_int_t njs_xml_node_ext_parent(njs_vm_t *vm, njs_object_prop_t *prop,
      njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_remove_all_attributes(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_xml_node_ext_remove_attribute(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);
+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);
+static njs_int_t njs_xml_node_ext_remove_text(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_xml_node_ext_set_attribute(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_xml_node_ext_set_text(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);
 static njs_int_t njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop,
      njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 static njs_int_t njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
 
+static njs_int_t njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current,
+    njs_str_t *name, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_tag_remove(njs_vm_t *vm, xmlNode *current,
+    njs_str_t *name);
+static njs_int_t njs_xml_node_tag_handler(njs_vm_t *vm, xmlNode *current,
+    njs_str_t *name, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_tags_handler(njs_vm_t *vm, xmlNode *current,
+    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,
+    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_encode_special_chars(njs_vm_t *vm, njs_str_t *src,
+    njs_str_t *out);
+static njs_int_t njs_xml_replace_node(njs_vm_t *vm, xmlNode *old,
+    xmlNode *current);
+static void njs_xml_node_cleanup(void *data);
+static void njs_xml_doc_cleanup(void *data);
+
 static njs_xml_nset_t *njs_xml_nset_create(njs_vm_t *vm, xmlDoc *doc,
     xmlNode *current, njs_xml_nset_type_t type);
 static njs_xml_nset_t *njs_xml_nset_add(njs_xml_nset_t *nset,
@@ -122,6 +156,29 @@ static njs_external_t  njs_ext_xml[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("serialize"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_ext_canonicalization,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("serializeToString"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_ext_canonicalization,
+            .magic8 = 3,
+        }
+    },
+
 };
 
 
@@ -171,11 +228,24 @@ static njs_external_t  njs_ext_xml_node[] = {
         .flags = NJS_EXTERN_SELF,
         .u.object = {
             .enumerable = 1,
+            .writable = 1,
+            .configurable = 1,
             .prop_handler = njs_xml_node_ext_prop_handler,
             .keys = njs_xml_node_ext_prop_keys,
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("addChild"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_add_child,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("$attrs"),
@@ -212,10 +282,78 @@ static njs_external_t  njs_ext_xml_node[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("removeAllAttributes"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_remove_all_attributes,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("removeAttribute"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_remove_attribute,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("removeChildren"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_remove_children,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("removeText"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_remove_text,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("setAttribute"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_set_attribute,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("setText"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_set_text,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("$tags"),
         .enumerable = 1,
+        .writable = 1,
+        .configurable = 1,
         .u.property = {
             .handler = njs_xml_node_ext_tags,
         }
@@ -225,6 +363,7 @@ static njs_external_t  njs_ext_xml_node[] = {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("$text"),
         .enumerable = 1,
+        .writable = 1,
         .u.property = {
             .handler = njs_xml_node_ext_text,
         }
@@ -410,16 +549,6 @@ njs_xml_doc_ext_root(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
 }
 
 
-static void
-njs_xml_doc_cleanup(void *data)
-{
-    njs_xml_doc_t  *current = data;
-
-    xmlFreeDoc(current->doc);
-    xmlFreeParserCtxt(current->ctx);
-}
-
-
 static njs_int_t
 njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys)
 {
@@ -515,15 +644,11 @@ njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys)
 
 static njs_int_t
 njs_xml_node_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop,
-    njs_value_t *value, njs_value_t *unused, njs_value_t *retval)
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
 {
-    size_t        size;
-    xmlAttr       *attr;
-    xmlNode       *node, *current;
-    njs_int_t     ret;
-    njs_str_t     name;
-    njs_value_t   *push;
-    const u_char  *content;
+    xmlNode    *current;
+    njs_int_t  ret;
+    njs_str_t  name;
 
     /*
      * $tag$foo - the first tag child with the name "foo"
@@ -548,108 +673,93 @@ njs_xml_node_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop,
         if (name.length > njs_length("$attr$")
             && njs_strncmp(&name.start[1], "attr$", njs_length("attr$")) == 0)
         {
-            for (attr = current->properties; attr != NULL; attr = attr->next) {
-                if (attr->type != XML_ATTRIBUTE_NODE) {
-                    continue;
-                }
-
-                size = njs_strlen(attr->name);
-
-                if (name.length != (size + njs_length("$attr$"))
-                    || njs_strncmp(&name.start[njs_length("$attr$")],
-                                   attr->name, size) != 0)
-                {
-                    continue;
-                }
-
-                content = (const u_char *) attr->children->content;
+            name.length -= njs_length("$attr$");
+            name.start += njs_length("$attr$");
 
-                return njs_vm_value_string_create(vm, retval, content,
-                                                  njs_strlen(content));
-            }
+            return njs_xml_node_attr_handler(vm, current, &name, setval,
+                                             retval);
         }
 
         if (name.length > njs_length("$tag$")
             && njs_strncmp(&name.start[1], "tag$", njs_length("tag$")) == 0)
         {
-            for (node = current->children; node != NULL; node = node->next) {
-                if (node->type != XML_ELEMENT_NODE) {
-                    continue;
-                }
-
-                size = njs_strlen(node->name);
-
-                if (name.length != (size + njs_length("$tag$"))
-                    || njs_strncmp(&name.start[njs_length("$tag$")],
-                                   node->name, size) != 0)
-                {
-                    continue;
-                }
-
-                return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
-                                              node, 0);
-            }
+            name.length -= njs_length("$tag$");
+            name.start += njs_length("$tag$");
+
+            return njs_xml_node_tag_handler(vm, current, &name, setval, retval);
         }
 
         if (name.length >= njs_length("$tags$")
             && njs_strncmp(&name.start[1], "tags$", njs_length("tags$")) == 0)
         {
-            ret = njs_vm_array_alloc(vm, retval, 2);
-            if (njs_slow_path(ret != NJS_OK)) {
-                return NJS_ERROR;
-            }
+            name.length -= njs_length("$tags$");
+            name.start += njs_length("$tags$");
 
-            for (node = current->children; node != NULL; node = node->next) {
-                if (node->type != XML_ELEMENT_NODE) {
-                    continue;
-                }
-
-                size = njs_strlen(node->name);
-
-                if (name.length > njs_length("$tags$")
-                    && (name.length != (size + njs_length("$tags$"))
-                        || njs_strncmp(&name.start[njs_length("$tags$")],
-                                       node->name, size) != 0))
-                {
-                    continue;
-                }
-
-                push = njs_vm_array_push(vm, retval);
-                if (njs_slow_path(push == NULL)) {
-                    return NJS_ERROR;
-                }
-
-                ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id,
-                                             node, 0);
-                if (njs_slow_path(ret != NJS_OK)) {
-                    return NJS_ERROR;
-                }
-            }
-
-            return NJS_OK;
+            return njs_xml_node_tags_handler(vm, current, &name, setval,
+                                             retval);
         }
     }
 
-    for (node = current->children; node != NULL; node = node->next) {
-        if (node->type != XML_ELEMENT_NODE) {
-            continue;
-        }
+    return njs_xml_node_tag_handler(vm, current, &name, setval, retval);
+}
 
-        size = njs_strlen(node->name);
 
-        if (name.length != size
-            || njs_strncmp(name.start, node->name, size) != 0)
-        {
-            continue;
-        }
+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)
+{
+    xmlNode    *current, *node, *copy;
+    njs_int_t  ret;
 
-        return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
-                                      node, 0);
+    current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0));
+    if (njs_slow_path(current == NULL)) {
+        njs_vm_error(vm, "\"this\" is not a XMLNode object");
+        return NJS_ERROR;
     }
 
-    njs_value_undefined_set(retval);
+    copy = xmlDocCopyNode(current, current->doc, 1);
+    if (njs_slow_path(copy == NULL)) {
+        njs_vm_error(vm, "xmlDocCopyNode() failed");
+        return NJS_ERROR;
+    }
 
-    return NJS_DECLINED;
+    node = njs_xml_external_node(vm, njs_arg(args, nargs, 1));
+    if (njs_slow_path(node == NULL)) {
+        njs_vm_error(vm, "node is not a XMLNode object");
+        goto error;
+    }
+
+    node = xmlDocCopyNode(node, current->doc, 1);
+    if (njs_slow_path(node == NULL)) {
+        njs_vm_error(vm, "xmlDocCopyNode() failed");
+        goto error;
+    }
+
+    node = xmlAddChild(copy, node);
+    if (njs_slow_path(node == NULL)) {
+        njs_vm_error(vm, "xmlAddChild() failed");
+        goto error;
+    }
+
+    ret = xmlReconciliateNs(current->doc, copy);
+    if (njs_slow_path(ret == -1)) {
+        njs_vm_error(vm, "xmlReconciliateNs() failed");
+        return NJS_ERROR;
+    }
+
+    njs_value_undefined_set(njs_vm_retval(vm));
+
+    return njs_xml_replace_node(vm, current, copy);
+
+error:
+
+    if (node != NULL) {
+        xmlFreeNode(node);
+    }
+
+    xmlFreeNode(copy);
+
+    return NJS_ERROR;
 }
 
 
@@ -725,121 +835,755 @@ njs_xml_node_ext_parent(njs_vm_t *vm, njs_object_prop_t *prop,
 
 
 static njs_int_t
-njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
-     njs_value_t *setval, njs_value_t *retval)
+njs_xml_node_ext_remove_attribute(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused)
 {
-    xmlNode      *node, *current;
-    njs_int_t    ret;
-    njs_value_t  *push;
+    return njs_xml_node_ext_set_attribute(vm, args, nargs, 1);
+}
 
-    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
-    if (njs_slow_path(current == NULL || current->children == NULL)) {
-        njs_value_undefined_set(retval);
-        return NJS_DECLINED;
-    }
 
-    ret = njs_vm_array_alloc(vm, retval, 2);
-    if (njs_slow_path(ret != NJS_OK)) {
+static njs_int_t
+njs_xml_node_ext_remove_all_attributes(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused)
+{
+    xmlNode  *current;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0));
+    if (njs_slow_path(current == NULL)) {
+        njs_vm_error(vm, "\"this\" is not a XMLNode object");
         return NJS_ERROR;
     }
 
-    for (node = current->children; node != NULL; node = node->next) {
-        if (node->type != XML_ELEMENT_NODE) {
-            continue;
-        }
-
-        push = njs_vm_array_push(vm, retval);
-        if (njs_slow_path(push == NULL)) {
-            return NJS_ERROR;
-        }
-
-        ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id, node, 0);
-        if (njs_slow_path(ret != NJS_OK)) {
-            return NJS_ERROR;
-        }
+    if (current->properties != NULL) {
+        xmlFreePropList(current->properties);
+        current->properties = NULL;
     }
 
+    njs_value_undefined_set(njs_vm_retval(vm));
+
     return NJS_OK;
 }
 
 
 static njs_int_t
-njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
-     njs_value_t *setval, njs_value_t *retval)
+njs_xml_node_ext_remove_children(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused)
 {
-    xmlNode    *current, *node;
-    njs_int_t  ret;
-    njs_chb_t  chain;
+    xmlNode      *current, *copy;
+    njs_str_t    name;
+    njs_value_t  *selector;
 
-    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0));
     if (njs_slow_path(current == NULL)) {
-        njs_value_undefined_set(retval);
-        return NJS_DECLINED;
+        njs_vm_error(vm, "\"this\" is not a XMLNode object");
+        return NJS_ERROR;
     }
 
-    njs_chb_init(&chain, njs_vm_memory_pool(vm));
+    selector = njs_arg(args, nargs, 1);
 
-    for (node = current->children; node != NULL; node = node->next) {
-        if (node->type != XML_TEXT_NODE) {
-            continue;
+    njs_value_undefined_set(njs_vm_retval(vm));
+
+    if (!njs_value_is_null_or_undefined(selector)) {
+        if (njs_slow_path(!njs_value_is_string(selector))) {
+            njs_vm_error(vm, "selector is not a string");
+            return NJS_ERROR;
         }
 
-        njs_chb_append(&chain, node->content, njs_strlen(node->content));
+        njs_value_string_get(selector, &name);
+
+        return njs_xml_node_tag_remove(vm, current, &name);
     }
 
-    ret = njs_vm_value_string_create_chb(vm, retval, &chain);
+    /* all. */
 
-    njs_chb_destroy(&chain);
+    copy = xmlDocCopyNode(current, current->doc, 1);
+    if (njs_slow_path(copy == NULL)) {
+        njs_vm_error(vm, "xmlDocCopyNode() failed");
+        return NJS_ERROR;
+    }
 
-    return ret;
+    if (copy->children != NULL) {
+        xmlFreeNodeList(copy->children);
+        copy->children = NULL;
+    }
+
+    return njs_xml_replace_node(vm, current, copy);
 }
 
 
-static int
-njs_xml_buf_write_cb(void *context, const char *buffer, int len)
+static njs_int_t
+njs_xml_node_ext_remove_text(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused)
 {
-    njs_chb_t  *chain = context;
-
-    njs_chb_append(chain, buffer, len);
-
-    return chain->error ? -1 : len;
+    return njs_xml_node_ext_text(vm, NULL, njs_argument(args, 0), NULL, NULL);
 }
 
 
-static int
-njs_xml_node_one_contains(njs_xml_nset_t *nset, xmlNode *node, xmlNode *parent)
+static njs_int_t
+njs_xml_node_ext_set_attribute(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t remove)
 {
-    int    in;
-    xmlNs  ns;
+    xmlNode      *current;
+    njs_str_t    str;
+    njs_value_t  *name;
 
-    if (nset->type == XML_NSET_TREE_NO_COMMENTS
-        && node->type == XML_COMMENT_NODE)
-    {
-        return 0;
+    current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 0));
+    if (njs_slow_path(current == NULL)) {
+        njs_vm_error(vm, "\"this\" is not a XMLNode object");
+        return NJS_ERROR;
     }
 
-    in = 1;
+    name = njs_arg(args, nargs, 1);
 
-    if (nset->nodes != NULL) {
-        if (node->type != XML_NAMESPACE_DECL) {
-            in = xmlXPathNodeSetContains(nset->nodes, node);
+    if (njs_slow_path(!njs_value_is_string(name))) {
+        njs_vm_error(vm, "name is not a string");
+        return NJS_ERROR;
+    }
 
-        } else {
+    njs_value_string_get(name, &str);
 
-            memcpy(&ns, node, sizeof(ns));
+    return njs_xml_node_attr_handler(vm, current, &str, njs_arg(args, nargs, 2),
+                                     !remove ? njs_vm_retval(vm) : NULL);
+}
 
-            /* libxml2 workaround, check xpath.c for details */
 
-            if ((parent != NULL) && (parent->type == XML_ATTRIBUTE_NODE)) {
-                ns.next = (xmlNs *) parent->parent;
+static njs_int_t
+njs_xml_node_ext_set_text(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    return njs_xml_node_ext_text(vm, NULL, njs_argument(args, 0),
+                                 njs_arg(args, nargs, 1), njs_vm_retval(vm));
+}
 
-            } else {
-                ns.next = (xmlNs *) parent;
-            }
 
-            in = xmlXPathNodeSetContains(nset->nodes, (xmlNode *) &ns);
-        }
-    }
+static njs_int_t
+njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
+     njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode    *current;
+    njs_str_t  name;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL || current->children == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    name.start = NULL;
+    name.length = 0;
+
+    return njs_xml_node_tags_handler(vm, current, &name, setval, retval);
+}
+
+
+static njs_int_t
+njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *unused,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    u_char     *text;
+    xmlNode    *current, *copy;
+    njs_int_t  ret;
+    njs_str_t  content, enc;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    if (retval != NULL && setval == NULL) {
+        text = xmlNodeGetContent(current);
+        ret = njs_vm_value_string_create(vm, retval, text, njs_strlen(text));
+
+        xmlFree(text);
+
+        return ret;
+    }
+
+    /* set or delete. */
+
+    enc.start = NULL;
+    enc.length = 0;
+
+    if (retval != NULL
+        && (setval != NULL && !njs_value_is_null_or_undefined(setval)))
+    {
+        if (njs_slow_path(!njs_value_is_string(setval))) {
+            njs_vm_error(vm, "setval is not a string");
+            return NJS_ERROR;
+        }
+
+        njs_value_string_get(setval, &content);
+
+        ret = njs_xml_encode_special_chars(vm, &content, &enc);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    copy = xmlDocCopyNode(current, current->doc, 1);
+    if (njs_slow_path(copy == NULL)) {
+        njs_vm_error(vm, "xmlDocCopyNode() failed");
+        return NJS_ERROR;
+    }
+
+    xmlNodeSetContentLen(copy, enc.start, enc.length);
+
+    if (retval != NULL) {
+        njs_value_undefined_set(retval);
+    }
+
+    return njs_xml_replace_node(vm, current, copy);
+}
+
+
+static njs_int_t
+njs_xml_node_attr_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name,
+    njs_value_t *setval, njs_value_t *retval)
+{
+    size_t        size;
+    njs_int_t     ret;
+    xmlAttr       *attr;
+    const u_char  *content, *value;
+    u_char        name_buf[512], value_buf[1024];
+
+    if (retval != NULL && setval == NULL) {
+        /* get. */
+
+        for (attr = current->properties; attr != NULL; attr = attr->next) {
+            if (attr->type != XML_ATTRIBUTE_NODE) {
+                continue;
+            }
+
+            size = njs_strlen(attr->name);
+
+            if (name->length != size
+                || njs_strncmp(name->start, attr->name, size) != 0)
+            {
+                continue;
+            }
+
+            if (attr->children != NULL
+                && attr->children->next == NULL
+                && attr->children->type == XML_TEXT_NODE)
+            {
+                content = (const u_char *) attr->children->content;
+
+                return njs_vm_value_string_create(vm, retval, content,
+                                                  njs_strlen(content));
+            }
+        }
+
+        njs_value_undefined_set(retval);
+
+        return NJS_DECLINED;
+    }
+
+    /* 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)) {
+        return NJS_ERROR;
+    }
+
+    ret = xmlValidateQName(&name_buf[0], 0);
+    if (njs_slow_path(ret != 0)) {
+        njs_vm_error(vm, "attribute name \"%V\" is not valid", name);
+        return NJS_ERROR;
+    }
+
+    if (retval == NULL
+        || (setval != NULL && njs_value_is_null_or_undefined(setval)))
+    {
+        /* delete. */
+
+        attr = xmlHasProp(current, &name_buf[0]);
+
+        if (attr != NULL) {
+            xmlRemoveProp(attr);
+        }
+
+        return NJS_OK;
+    }
+
+    value = njs_xml_value_to_c_string(vm, setval, &value_buf[0],
+                                      sizeof(value_buf));
+    if (njs_slow_path(value == NULL)) {
+        return NJS_ERROR;
+    }
+
+    attr = xmlSetProp(current, &name_buf[0], value);
+    if (njs_slow_path(attr == NULL)) {
+        njs_vm_error(vm, "xmlSetProp() failed");
+        return NJS_ERROR;
+    }
+
+    njs_value_undefined_set(retval);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_node_tag_remove(njs_vm_t *vm, xmlNode *current, njs_str_t *name)
+{
+    size_t     size;
+    xmlNode    *node, *next, *copy;
+    njs_int_t  ret;
+
+    copy = xmlDocCopyNode(current, current->doc, 1);
+    if (njs_slow_path(copy == NULL)) {
+        njs_vm_error(vm, "xmlDocCopyNode() failed");
+        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)
+        {
+            continue;
+        }
+
+        ret = njs_xml_replace_node(vm, node, NULL);
+        if (njs_slow_path(ret != NJS_OK)) {
+            xmlFreeNode(copy);
+            return NJS_ERROR;
+        }
+    }
+
+    return njs_xml_replace_node(vm, current, copy);
+}
+
+
+static njs_int_t
+njs_xml_node_tag_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name,
+    njs_value_t *setval, njs_value_t *retval)
+{
+    size_t   size;
+    xmlNode  *node;
+
+    if (retval != NULL && setval == NULL) {
+
+        /* get. */
+
+        for (node = current->children; node != NULL; node = 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)
+            {
+                continue;
+            }
+
+            return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
+                                          node, 0);
+        }
+
+        njs_value_undefined_set(retval);
+
+        return NJS_DECLINED;
+    }
+
+    if (retval != NULL) {
+        njs_vm_error(vm, "XMLNode.$tag$xxx is not assignable, use addChild() or"
+                     "node.$tags = [node1, node2, ..] syntax");
+        return NJS_ERROR;
+    }
+
+    /* delete. */
+
+    return njs_xml_node_tag_remove(vm, current, name);
+}
+
+
+static njs_int_t
+njs_xml_node_tags_handler(njs_vm_t *vm, xmlNode *current, njs_str_t *name,
+    njs_value_t *setval, njs_value_t *retval)
+{
+    size_t       size;
+    int64_t      i, length;
+    xmlNode      *node, *copy;
+    njs_int_t    ret;
+    njs_value_t  *push;
+    njs_opaque_value_t  *start;
+
+    if (retval != NULL && setval == NULL) {
+
+        /* get. */
+
+        ret = njs_vm_array_alloc(vm, retval, 2);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+
+        for (node = current->children; node != NULL; node = node->next) {
+            if (node->type != XML_ELEMENT_NODE) {
+                continue;
+            }
+
+            size = njs_strlen(node->name);
+
+            if (name->length > 0
+                && (name->length != size
+                    || njs_strncmp(name->start, node->name, size) != 0))
+            {
+                continue;
+            }
+
+            push = njs_vm_array_push(vm, retval);
+            if (njs_slow_path(push == NULL)) {
+                return NJS_ERROR;
+            }
+
+            ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id,
+                                         node, 0);
+            if (njs_slow_path(ret != NJS_OK)) {
+                return NJS_ERROR;
+            }
+        }
+
+        return NJS_OK;
+    }
+
+    if (name->length > 0) {
+        njs_vm_error(vm, "XMLNode $tags$xxx is not assignable, use addChild() "
+                     "or node.$tags = [node1, node2, ..] syntax");
+        return NJS_ERROR;
+    }
+
+    /* set or delete. */
+
+    copy = xmlDocCopyNode(current, current->doc, 1);
+    if (njs_slow_path(copy == NULL)) {
+        njs_vm_error(vm, "xmlDocCopyNode() failed");
+        return NJS_ERROR;
+    }
+
+    if (copy->children != NULL) {
+        xmlFreeNodeList(copy->children);
+        copy->children = NULL;
+    }
+
+    if (retval == NULL) {
+        /* delete. */
+        return njs_xml_replace_node(vm, current, copy);
+    }
+
+    if (!njs_value_is_array(setval)) {
+        njs_vm_error(vm, "setval is not an array");
+        goto error;
+    }
+
+    start = (njs_opaque_value_t *) njs_vm_array_start(vm, setval);
+    if (njs_slow_path(start == NULL)) {
+        goto error;
+    }
+
+    (void) njs_vm_array_length(vm, setval, &length);
+
+    for (i = 0; i < length; i++) {
+        node = njs_xml_external_node(vm, njs_value_arg(start++));
+        if (njs_slow_path(node == NULL)) {
+            njs_vm_error(vm, "setval[%D] is not a XMLNode object", i);
+            goto error;
+        }
+
+        node = xmlDocCopyNode(node, current->doc, 1);
+        if (njs_slow_path(node == NULL)) {
+            njs_vm_error(vm, "xmlDocCopyNode() failed");
+            goto error;
+        }
+
+        node = xmlAddChild(copy, node);
+        if (njs_slow_path(node == NULL)) {
+            njs_vm_error(vm, "xmlAddChild() failed");
+            xmlFreeNode(node);
+            goto error;
+        }
+
+        ret = xmlReconciliateNs(current->doc, copy);
+        if (njs_slow_path(ret == -1)) {
+            njs_vm_error(vm, "xmlReconciliateNs() failed");
+            return NJS_ERROR;
+        }
+    }
+
+    njs_value_undefined_set(retval);
+
+    return njs_xml_replace_node(vm, current, copy);
+
+error:
+
+    xmlFreeNode(copy);
+
+    return NJS_ERROR;
+}
+
+
+static xmlNode *
+njs_xml_external_node(njs_vm_t *vm, njs_value_t *value)
+{
+    xmlNode        *current;
+    njs_xml_doc_t  *tree;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        tree = njs_vm_external(vm, njs_xml_doc_proto_id, value);
+        if (njs_slow_path(tree == NULL)) {
+            njs_vm_error(vm, "\"this\" is not a XMLNode object");
+            return NULL;
+        }
+
+        current = xmlDocGetRootElement(tree->doc);
+        if (njs_slow_path(current == NULL)) {
+            njs_vm_error(vm, "\"this\" is not a XMLNode object");
+            return NULL;
+        }
+    }
+
+    return current;
+}
+
+
+static njs_int_t
+njs_xml_str_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_error(vm, "njs_xml_str_to_c_string() very long string, "
+                     "length >= %uz", size - njs_length("\0"));
+        return NJS_ERROR;
+    }
+
+    p = njs_cpymem(dst, str->start, str->length);
+    *p = '\0';
+
+    return NJS_OK;
+}
+
+
+static const u_char *
+njs_xml_value_to_c_string(njs_vm_t *vm, njs_value_t *value, u_char *dst,
+    size_t size)
+{
+    u_char     *p;
+    njs_str_t  str;
+    njs_int_t  ret;
+
+    ret = njs_vm_value_to_bytes(vm, &str, value);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NULL;
+    }
+
+    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;
+        }
+
+        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;
+    }
+
+    p = njs_cpymem(dst, str.start, str.length);
+    *p = '\0';
+
+    return dst;
+}
+
+
+static njs_int_t
+njs_xml_encode_special_chars(njs_vm_t *vm, njs_str_t *src, njs_str_t *out)
+{
+    u_char  *p, *dst, *end;
+    size_t   len;
+
+    len = 0;
+    end = src->start + src->length;
+
+    for (p = src->start; p < end; p++) {
+        if (*p == '<' || *p == '>') {
+            len += njs_length("&lt");
+        }
+
+        if (*p == '&' || *p == '\r') {
+            len += njs_length("&amp");
+        }
+
+        if (*p == '"') {
+            len += njs_length("&quot");
+        }
+
+        len += 1;
+    }
+
+    if (njs_fast_path(len == src->length)) {
+        *out = *src;
+        return NJS_OK;
+    }
+
+    out->start = njs_mp_alloc(njs_vm_memory_pool(vm), len);
+    if (njs_slow_path(out->start == NULL)) {
+        njs_vm_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    dst = out->start;
+
+    for (p = src->start; p < end; p++) {
+        if (*p == '<') {
+            *dst++ = '&';
+            *dst++ = 'l';
+            *dst++ = 't';
+            *dst++ = ';';
+
+        } else if (*p == '>') {
+            *dst++ = '&';
+            *dst++ = 'g';
+            *dst++ = 't';
+            *dst++ = ';';
+
+        } else if (*p == '&') {
+            *dst++ = '&';
+            *dst++ = 'a';
+            *dst++ = 'm';
+            *dst++ = 'p';
+            *dst++ = ';';
+
+        } else if (*p == '"') {
+            *dst++ = '&';
+            *dst++ = 'q';
+            *dst++ = 'u';
+            *dst++ = 'o';
+            *dst++ = 't';
+            *dst++ = ';';
+
+        } else if (*p == '\r') {
+            *dst++ = '&';
+            *dst++ = '#';
+            *dst++ = '1';
+            *dst++ = '3';
+            *dst++ = ';';
+
+        } else {
+            *dst++ = *p;
+        }
+    }
+
+    out->length = len;
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_replace_node(njs_vm_t *vm, xmlNode *old, xmlNode *current)
+{
+    njs_mp_cleanup_t  *cln;
+
+    if (current != NULL) {
+        old = xmlReplaceNode(old, current);
+
+    } else {
+        xmlUnlinkNode(old);
+    }
+
+    cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+    if (njs_slow_path(cln == NULL)) {
+        njs_vm_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    cln->handler = njs_xml_node_cleanup;
+    cln->data = old;
+
+    return NJS_OK;
+}
+
+
+static void
+njs_xml_node_cleanup(void *data)
+{
+    xmlNode *current = data;
+
+    xmlFreeNode(current);
+}
+
+
+static void
+njs_xml_doc_cleanup(void *data)
+{
+    njs_xml_doc_t  *current = data;
+
+    xmlFreeDoc(current->doc);
+    xmlFreeParserCtxt(current->ctx);
+}
+
+
+static int
+njs_xml_buf_write_cb(void *context, const char *buffer, int len)
+{
+    njs_chb_t  *chain = context;
+
+    njs_chb_append(chain, buffer, len);
+
+    return chain->error ? -1 : len;
+}
+
+
+static int
+njs_xml_node_one_contains(njs_xml_nset_t *nset, xmlNode *node, xmlNode *parent)
+{
+    int    in;
+    xmlNs  ns;
+
+    if (nset->type == XML_NSET_TREE_NO_COMMENTS
+        && node->type == XML_COMMENT_NODE)
+    {
+        return 0;
+    }
+
+    in = 1;
+
+    if (nset->nodes != NULL) {
+        if (node->type != XML_NAMESPACE_DECL) {
+            in = xmlXPathNodeSetContains(nset->nodes, node);
+
+        } else {
+
+            memcpy(&ns, node, sizeof(ns));
+
+            /* libxml2 workaround, check xpath.c for details */
+
+            if ((parent != NULL) && (parent->type == XML_ATTRIBUTE_NODE)) {
+                ns.next = (xmlNs *) parent->parent;
+
+            } else {
+                ns.next = (xmlNs *) parent;
+            }
+
+            in = xmlXPathNodeSetContains(nset->nodes, (xmlNode *) &ns);
+        }
+    }
 
     switch (nset->type) {
     case XML_NSET_TREE:
@@ -961,7 +1705,7 @@ njs_xml_parse_ns_list(njs_vm_t *vm, njs_str_t *src)
 
 static njs_int_t
 njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
-    njs_index_t exclusive)
+    njs_index_t magic)
 {
     u_char           **prefix_list;
     ssize_t          size;
@@ -971,23 +1715,12 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_chb_t        chain;
     njs_bool_t       comments;
     njs_value_t      *excluding, *prefixes;
-    njs_xml_doc_t    *tree;
     njs_xml_nset_t   *nset, *children;
     xmlOutputBuffer  *buf;
 
-    current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 1));
+    current = njs_xml_external_node(vm, njs_argument(args, 1));
     if (njs_slow_path(current == NULL)) {
-        tree = njs_vm_external(vm, njs_xml_doc_proto_id, njs_argument(args, 1));
-        if (njs_slow_path(tree == NULL)) {
-            njs_vm_error(vm, "\"this\" is not a XMLNode object");
-            return NJS_ERROR;
-        }
-
-        current = xmlDocGetRootElement(tree->doc);
-        if (njs_slow_path(current == NULL)) {
-            njs_vm_error(vm, "\"this\" is not a XMLNode object");
-            return NJS_ERROR;
-        }
+        return NJS_ERROR;
     }
 
     comments = njs_value_bool(njs_arg(args, nargs, 3));
@@ -1053,7 +1786,7 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     }
 
     ret = xmlC14NExecute(current->doc, njs_xml_c14n_visibility_cb, nset,
-                         exclusive ? XML_C14N_EXCLUSIVE_1_0 : XML_C14N_1_0,
+                         magic & 0x1 ? XML_C14N_EXCLUSIVE_1_0 : XML_C14N_1_0,
                          prefix_list, comments, buf);
 
     if (njs_slow_path(ret < 0)) {
@@ -1062,21 +1795,26 @@ njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         goto error;
     }
 
-    size = njs_chb_size(&chain);
-    if (njs_slow_path(size < 0)) {
-        njs_vm_memory_error(vm);
-        ret = NJS_ERROR;
-        goto error;
-    }
+    if (magic & 0x2) {
+        ret = njs_vm_value_string_create_chb(vm, njs_vm_retval(vm), &chain);
 
-    ret = njs_chb_join(&chain, &data);
-    if (njs_slow_path(ret != NJS_OK)) {
-        ret = NJS_ERROR;
-        goto error;
-    }
+    } else {
+        size = njs_chb_size(&chain);
+        if (njs_slow_path(size < 0)) {
+            njs_vm_memory_error(vm);
+            ret = NJS_ERROR;
+            goto error;
+        }
+
+        ret = njs_chb_join(&chain, &data);
+        if (njs_slow_path(ret != NJS_OK)) {
+            ret = NJS_ERROR;
+            goto error;
+        }
 
-    ret = njs_vm_value_buffer_set(vm, njs_vm_retval(vm), data.start,
-                                  data.length);
+        ret = njs_vm_value_buffer_set(vm, njs_vm_retval(vm), data.start,
+                                      data.length);
+    }
 
 error:
 
index a7651390cc340686f3a7a93905ea27b3bcef831f..88e86f73e22e56a06bbeab59c2ed56e875ce8def 100644 (file)
@@ -21819,10 +21819,14 @@ static njs_unit_test_t  njs_webcrypto_test[] =
 };
 
 
+#define NJS_XML_DOC "const xml = require('xml');" \
+                    "let data = `<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`;" \
+                    "let doc = xml.parse(data);"
+
+
 static njs_unit_test_t  njs_xml_test[] =
 {
-    { njs_str("const xml = require('xml');"
-              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+    { njs_str(NJS_XML_DOC
               "[doc.note.$name,"
               " doc.note.to.$text,"
               " doc.note.$parent,"
@@ -21871,20 +21875,26 @@ static njs_unit_test_t  njs_xml_test[] =
               "<n1:elem1 xmlns:n1=\"http://b\"><!-- comment -->foo</n1:elem1>,"
               "<n1:elem1 xmlns:n0=\"http://a\" xmlns:n1=\"http://b\">foo</n1:elem1>") },
 
-    { njs_str("const xml = require('xml');"
-              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+    { njs_str(NJS_XML_DOC
               "let dec = new TextDecoder();"
               "dec.decode(xml.exclusiveC14n(doc.note))"),
       njs_str("<note><to a=\"foo\" b=\"bar\">Tove</to><from>Jani</from></note>") },
 
-    { njs_str("const xml = require('xml');"
-              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+    { njs_str(NJS_XML_DOC
+              "let dec = new TextDecoder();"
+              "dec.decode(xml.serialize(doc.note))"),
+      njs_str("<note><to a=\"foo\" b=\"bar\">Tove</to><from>Jani</from></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "xml.serializeToString(doc.note)"),
+      njs_str("<note><to a=\"foo\" b=\"bar\">Tove</to><from>Jani</from></note>") },
+
+    { njs_str(NJS_XML_DOC
               "let dec = new TextDecoder();"
               "dec.decode(xml.exclusiveC14n(doc.note, doc.note.to))"),
       njs_str("<note><from>Jani</from></note>") },
 
-    { njs_str("const xml = require('xml');"
-              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+    { njs_str(NJS_XML_DOC
               "njs.dump(doc)"),
       njs_str("XMLDoc {note:XMLNode {$name:'note',"
               "$tags:[XMLNode {$name:'to',"
@@ -21892,8 +21902,7 @@ static njs_unit_test_t  njs_xml_test[] =
               "$text:'Tove'},"
               "XMLNode {$name:'from',$text:'Jani'}]}}") },
 
-    { njs_str("const xml = require('xml');"
-              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+    { njs_str(NJS_XML_DOC
               "JSON.stringify(doc)"),
       njs_str("{\"note\":{\"$name\":\"note\",\"$tags\":"
               "[{\"$name\":\"to\",\"$attrs\":{\"b\":\"bar\",\"a\":\"foo\"},"
@@ -21902,6 +21911,180 @@ static njs_unit_test_t  njs_xml_test[] =
     { njs_str("var xml = require('xml');"
               "var doc = xml.parse(`<r></r>`); xml.exclusiveC14n(doc, 1)"),
       njs_str("Error: \"excluding\" argument is not a XMLNode object") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text"),
+      njs_str("ToveJani") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = 'WAKA';"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("WAKA,<note>WAKA</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.setText('WAKA');"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("WAKA,<note>WAKA</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = '<WA&KA>';"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("<WA&KA>,<note>&lt;WA&amp;KA&gt;</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.setText('<WA&KA>');"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("<WA&KA>,<note>&lt;WA&amp;KA&gt;</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = '\"WAKA\"';"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("\"WAKA\",<note>\"WAKA\"</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = '';"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str(",<note></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.setText();"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str(",<note></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.setText(null);"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str(",<note></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.removeText();"
+              "[doc.$root.$text, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str(",<note></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "let to = doc.note.to;"
+              "doc.$root.$text = '';"
+              "[to.$name, to.$text, to.$attr$b, to.$parent.$name]"),
+      njs_str("to,Tove,bar,note") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = 'WAKA';"
+              "doc.$root.$attr$aaa = 'foo';"
+              "doc.$root.$attr$bbb = 'bar';"
+              "[doc.$root.$attr$aaa, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("foo,<note aaa=\"foo\" bbb=\"bar\">WAKA</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = 'WAKA';"
+              "doc.$root.setAttribute('aaa', 'foo');"
+              "doc.$root.setAttribute('bbb', '<bar\"');"
+              "doc.$root.setAttribute('aaa', 'foo2');"
+              "[doc.$root.$attr$aaa, (new TextDecoder).decode(xml.c14n(doc))]"),
+      njs_str("foo2,<note aaa=\"foo2\" bbb=\"&lt;bar&quot;\">WAKA</note>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.note.to.setAttribute('a', null);"
+              "(new TextDecoder).decode(xml.c14n(doc.note.to))"),
+      njs_str("<to b=\"bar\">Tove</to>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.setAttribute('<', 'xxx')"),
+      njs_str("Error: attribute name \"<\" is not valid") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.$root.$text = 'WAKA';"
+              "doc.$root['$attr$' + 'x'.repeat(1024)] = 1;"),
+      njs_str("Error: njs_xml_str_to_c_string() very long string, length >= 511") },
+
+    { njs_str(NJS_XML_DOC
+              "delete doc.note.to.$attr$a;"
+              "(new TextDecoder).decode(xml.c14n(doc.note.to))"),
+      njs_str("<to b=\"bar\">Tove</to>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.note.to.removeAttribute('a');"
+              "(new TextDecoder).decode(xml.c14n(doc.note.to))"),
+      njs_str("<to b=\"bar\">Tove</to>") },
+
+    { njs_str(NJS_XML_DOC
+              "delete doc.note.to.removeAttribute('c');"
+              "(new TextDecoder).decode(xml.c14n(doc.note.to))"),
+      njs_str("<to a=\"foo\" b=\"bar\">Tove</to>") },
+
+    { njs_str(NJS_XML_DOC
+              "delete doc.note.to.removeAllAttributes();"
+              "(new TextDecoder).decode(xml.c14n(doc.note.to))"),
+      njs_str("<to>Tove</to>") },
+
+    { njs_str(NJS_XML_DOC
+              "delete doc.note.$tag$to;"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note><from>Jani</from></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "delete doc.note.to;"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note><from>Jani</from></note>") },
+
+    { njs_str("var xml = require('xml');"
+              "var doc = xml.parse(`<r><a/><b/><a/></r>`);"
+              "delete doc.$root.a;"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<r><b></b></r>") },
+
+    { njs_str("var xml = require('xml');"
+              "var doc = xml.parse(`<r><a/><b/><a/></r>`);"
+              "doc.$root.removeChildren('c');"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<r><a></a><b></b><a></a></r>") },
+
+    { njs_str("var xml = require('xml');"
+              "var doc = xml.parse(`<r><a/><b/><a/></r>`);"
+              "doc.$root.removeChildren('a');"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<r><b></b></r>") },
+
+    { njs_str("var xml = require('xml');"
+              "var doc = xml.parse(`<r><a/><b/><a/></r>`);"
+              "doc.$root.removeChildren();"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<r></r>") },
+
+    { njs_str(NJS_XML_DOC
+              "doc.note.$tags = [];"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "var doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);"
+              "doc.note.addChild(doc2);"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note xmlns:n0=\"http://a\"><to a=\"foo\" b=\"bar\">Tove</to><from>Jani</from><n0:pdu></n0:pdu></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "var doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);"
+              "doc.note.addChild(doc2);"
+              "doc.note.addChild(doc2);"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note xmlns:n0=\"http://a\"><to a=\"foo\" b=\"bar\">Tove</to><from>Jani</from>"
+              "<n0:pdu></n0:pdu><n0:pdu></n0:pdu></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "delete doc.note.$tags$;"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "var doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);"
+              "doc.note.$tags = [doc.note.to, doc2];"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note xmlns:n0=\"http://a\"><to a=\"foo\" b=\"bar\">Tove</to><n0:pdu></n0:pdu></note>") },
+
+    { njs_str(NJS_XML_DOC
+              "var doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);"
+              "doc.note.$tags = [doc2, doc.note.to];"
+              "(new TextDecoder).decode(xml.c14n(doc))"),
+      njs_str("<note xmlns:n0=\"http://a\"><n0:pdu></n0:pdu><to a=\"foo\" b=\"bar\">Tove</to></note>") },
 };
 
 
index 9a2d98a58515b35715566ecde806421f800bf75f..ec15c513325d29f833923911118b343cd658d067 100644 (file)
@@ -6,6 +6,14 @@ flags: [async]
 async function verify(params) {
     let file_data = fs.readFileSync(`test/xml/${params.saml}`);
     let key_data = fs.readFileSync(`test/webcrypto/${params.key.file}`);
+
+    if (params.sign) {
+        let sign_key_data = fs.readFileSync(`test/webcrypto/${params.key.sign_file}`);
+        let signed = await signSAML(xml.parse(file_data), sign_key_data);
+        file_data = xml.c14n(signed);
+        //console.log((new TextDecoder()).decode(file_data));
+    }
+
     let saml = xml.parse(file_data);
 
     let r = await verifySAMLSignature(saml, key_data)
@@ -27,6 +35,62 @@ async function verify(params) {
     return 'SUCCESS';
 }
 
+/*
+ * signSAML() signs a SAML message template
+ *
+ *  The message to sign should already contain a full Signature element
+ *  with SignedInfo and Reference elements below.
+ *  The only parts that are missing are DigestValue and SignatureValue.
+ *   <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ *       <ds:SignedInfo>
+ *           <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ *           <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
+ *           <ds:Reference URI="#_0x14956c887e664bdb71d7685b89b70619">
+ *               <ds:Transforms>
+ *                   <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ *                   <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ *               </ds:Transforms>
+ *               <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+ *               <ds:DigestValue></ds:DigestValue>
+ *           </ds:Reference>
+ *       </ds:SignedInfo>
+ *       <ds:SignatureValue></ds:SignatureValue>
+ *   </ds:Signature>
+ *
+ * The following signature algorithms are supported:
+ * - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
+ * - http://www.w3.org/2000/09/xmldsig#rsa-sha1
+ *
+ * The following digest algorithms are supported:
+ * - http://www.w3.org/2000/09/xmldsig#sha1
+ * - http://www.w3.org/2001/04/xmlenc#sha256
+ *
+ * As a part of the signing process, the following attributes are set:
+ * - xml:ID
+ *   The value is a random 16 bytes hex string.
+ * - IssueInstant
+ *   The value is the current time in ISO 8601 format.
+ *
+ * @param doc an XMLDoc object returned by xml.parse().
+ * @param key_data is PKCS #8 in PEM format.
+ */
+async function signSAML(saml, key_data) {
+    const root = saml.$root;
+    const rootSignature = root.Signature;
+
+    const rnd = Buffer.alloc(16);
+    crypto.getRandomValues(rnd);
+    const id = rnd.toString('hex');
+
+    root.setAttribute('xml:ID', id);
+    rootSignature.SignedInfo.Reference.setAttribute('URI', `#${id}`);
+    root.setAttribute('IssueInstant', (new Date()).toISOString());
+
+    await digestSAML(rootSignature, true);
+    await signatureSAML(rootSignature, key_data, true);
+    return saml;
+}
+
 /*
  * verifySAMLSignature() implements a verify clause
  * from Profiles for the OASIS SAML V2.0
@@ -61,21 +125,21 @@ async function verifySAMLSignature(saml, key_data) {
     const assertionSignature = assertion ? assertion.Signature : null;
 
     if (assertionSignature) {
-        if (!await verifyDigest(assertionSignature)) {
+        if (!await digestSAML(assertionSignature)) {
             return false;
         }
 
-        if (!await verifySignature(assertionSignature, key_data)) {
+        if (!await signatureSAML(assertionSignature, key_data)) {
             return false;
         }
     }
 
     if (rootSignature) {
-        if (!await verifyDigest(rootSignature)) {
+        if (!await digestSAML(rootSignature)) {
             return false;
         }
 
-        if (!await verifySignature(rootSignature, key_data)) {
+        if (!await signatureSAML(rootSignature, key_data)) {
             return false;
         }
     }
@@ -83,7 +147,7 @@ async function verifySAMLSignature(saml, key_data) {
     return true;
 }
 
-async function verifyDigest(signature) {
+async function digestSAML(signature, produce) {
     const parent = signature.$parent;
     const signedInfo = signature.SignedInfo;
     const reference = signedInfo.Reference;
@@ -137,12 +201,17 @@ async function verifyDigest(signature) {
         throw Error(`unexpected digest Algorithm ${alg}`);
     }
 
-    const expectedDigest = signedInfo.Reference.DigestValue.$text;
-
     const c14n = xml.exclusiveC14n(parent, signature, withComments, prefixList);
     const dgst = await crypto.subtle.digest(hash, c14n);
     const b64dgst = Buffer.from(dgst).toString('base64');
 
+    if (produce) {
+        signedInfo.Reference.DigestValue.$text = b64dgst;
+        return b64dgst;
+    }
+
+    const expectedDigest = signedInfo.Reference.DigestValue.$text;
+
     return expectedDigest === b64dgst;
 }
 
@@ -159,9 +228,7 @@ function base64decode(b64) {
     return Buffer.from(joined, 'base64');
 }
 
-async function verifySignature(signature, key_data) {
-    const der = keyPem2Der(key_data, "PUBLIC");
-
+async function signatureSAML(signature, key_data, produce) {
     let method, hash;
     const signedInfo = signature.SignedInfo;
     const alg = signedInfo.SignatureMethod.$attr$Algorithm;
@@ -179,15 +246,27 @@ async function verifySignature(signature, key_data) {
         throw Error(`unexpected signature Algorithm ${alg}`);
     }
 
-    const expectedValue = base64decode(signature.SignatureValue.$text);
     const withComments = signedInfo.CanonicalizationMethod
                          .$attr$Algorithm.slice(39) == 'WithComments';
 
     const signedInfoC14n = xml.exclusiveC14n(signedInfo, null, withComments);
 
+    if (produce) {
+        const der = keyPem2Der(key_data, "PRIVATE");
+        const key = await crypto.subtle.importKey("pkcs8", der, { name: method, hash },
+                                                  false, [ "sign" ]);
+
+        let sig =  await crypto.subtle.sign({ name: method }, key, signedInfoC14n);
+
+        signature.SignatureValue.$text = Buffer.from(sig).toString('base64');
+        return signature;
+    }
+
+    const der = keyPem2Der(key_data, "PUBLIC");
     const key = await crypto.subtle.importKey("spki", der, { name: method, hash },
-                                            false, [ "verify" ]);
+                                              false, [ "verify" ]);
 
+    const expectedValue = base64decode(signature.SignatureValue.$text);
     return await crypto.subtle.verify({ name: method }, key, expectedValue,
                                       signedInfoC14n);
 }
@@ -220,6 +299,8 @@ let saml_verify_tsuite = {
         { saml: "response_signed_broken2.xml", expected: false },
         { saml: "response_signed.xml", key: { file: "rsa2.spki"}, expected: false },
         { saml: "response_assertion_and_message_signed.xml", expected: true },
+
+        { saml: "auth_r.xml", sign: true, key: { sign_file: "rsa.pkcs8" }, expected: true },
 ]};
 
 run([
index 61c707ac4f48a5db5f78d46d32bfd9c92cab510a..39082ba398a63a8a340941ef171c6fecf8e15b8d 100644 (file)
@@ -72,8 +72,9 @@ declare module "xml" {
     export interface XMLNode {
         /**
          * node.$attr$xxx - the node's attribute value of "xxx".
+         * @since 0.7.11 the property is writable.
          */
-        readonly [key: `$attr$${string}`]: string | undefined;
+        [key: `$attr$${string}`]: string | undefined;
 
         /**
          * node.$attrs - an XMLAttr wrapper object for all the attributes
@@ -83,13 +84,15 @@ declare module "xml" {
 
         /**
          * node.$tag$xxx - the node's first child tag named "xxx".
+         * @since 0.7.11 the property is writable.
          */
-        readonly [key: `$tag$${string}`]: XMLNode | undefined;
+        [key: `$tag$${string}`]: XMLNode | undefined;
 
         /**
          * node.$tags$xxx - all children tags named "xxx" of the node.
+         * @since 0.7.11 the property is writable.
          */
-        readonly [key: `$tags$${string}`]: XMLNode[] | undefined;
+        [key: `$tags$${string}`]: XMLNode[] | undefined;
 
         /**
          * node.$name - the name of the node.
@@ -108,8 +111,9 @@ declare module "xml" {
 
         /**
          * node.$text - the content of the node.
+         * @since 0.7.11 the property is writable.
          */
-        readonly $text: string;
+        $text: string;
 
         /**
          * node.$tags - all the node's children tags.
@@ -118,8 +122,9 @@ declare module "xml" {
 
         /**
          * node.xxx is the same as node.$tag$xxx.
+         * @since 0.7.11 the property is writable.
          */
-        readonly [key: XMLTagName]: XMLNode | undefined;
+        [key: XMLTagName]: XMLNode | undefined;
     }
 
     export interface XMLAttr {
@@ -130,6 +135,15 @@ declare module "xml" {
     }
 
     interface Xml {
+        /**
+         * Canonicalizes root_node and its children according to
+         * https://www.w3.org/TR/xml-c14n/.
+         *
+         * @param root - XMLDoc or XMLNode.
+         * @return Buffer object containing canonicalized output.
+         */
+        c14n(root: XMLDoc | XMLNode): Buffer;
+
         /**
          * Parses src buffer for an XML document and returns a wrapper object.
          *
@@ -140,20 +154,32 @@ declare module "xml" {
 
         /**
          * Canonicalizes root_node and its children according to
-         * https://www.w3.org/TR/xml-exc-c14n/.
+         * https://www.w3.org/tr/xml-exc-c14n/.
          *
          * @param root - XMLDoc or XMLNode.
          * @param excluding_node - allows to omit from the output a part of the
          * document corresponding to the excluding_node and its children.
-         * @param withComments - a boolean (false by default). When withComments
+         * @param withComments - a boolean (false by default). when withComments
          * is true canonicalization corresponds to
          * http://www.w3.org/2001/10/xml-exc-c14n#WithComments.
          * @param prefix_list - an optional string with a space separated namespace
          * prefixes for namespaces that should also be included into the output.
-         * @return Buffer object containing canonicalized output.
+         * @return buffer object containing canonicalized output.
          */
         exclusiveC14n(root: XMLDoc | XMLNode, excluding_node?: XMLNode | null | undefined,
                       withComments?: boolean, prefix_list?: string): Buffer;
+
+        /**
+         * The alias to xml.x14n()
+         * @since 0.7.11
+         */
+        serialize(root: XMLDoc | XMLNode): Buffer;
+
+        /**
+         * The same as xml.x14n() but returns the retval as a string.
+         * @since 0.7.11
+         */
+        serializeToString(root: XMLDoc | XMLNode): string;
     }
 
     const xml: Xml;