[njs] QuickJS: added xml module.
noreply at nginx.com
noreply at nginx.com
Thu Mar 27 16:05:02 UTC 2025
details: https://github.com/nginx/njs/commit/cec9a1650a5a46bbede4a0b7abd750d25df19f28
branches: master
commit: cec9a1650a5a46bbede4a0b7abd750d25df19f28
user: Dmitry Volyntsev <xeioex at nginx.com>
date: Tue, 18 Mar 2025 16:22:53 -0700
description:
QuickJS: added xml module.
---
.github/workflows/check-pr.yml | 2 +-
auto/qjs_modules | 8 +
external/qjs_xml_module.c | 2161 +++++++++++++++++++++++++++++++++
src/qjs.h | 5 +-
src/test/njs_unit_test.c | 284 -----
test/xml/external_entity_ignored.t.js | 6 +-
test/xml/saml_verify.t.mjs | 9 +-
test/xml/xml.t.mjs | 385 ++++++
8 files changed, 2568 insertions(+), 292 deletions(-)
diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml
index 7cff7937..75e590fe 100644
--- a/.github/workflows/check-pr.yml
+++ b/.github/workflows/check-pr.yml
@@ -32,7 +32,7 @@ jobs:
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
- sudo apt-get install -y gcc-multilib libc6:i386 libpcre2-dev:i386 zlib1g-dev:i386
+ sudo apt-get install -y gcc-multilib libc6:i386 libssl-dev:i386 libpcre2-dev:i386 zlib1g-dev:i386 libxml2-dev:i386
- name: Check out nginx
run: |
diff --git a/auto/qjs_modules b/auto/qjs_modules
index 1381d2c5..03501d7d 100644
--- a/auto/qjs_modules
+++ b/auto/qjs_modules
@@ -33,6 +33,14 @@ if [ $NJS_OPENSSL = YES -a $NJS_HAVE_OPENSSL = YES ]; then
. auto/qjs_module
fi
+if [ $NJS_LIBXML2 = YES -a $NJS_HAVE_LIBXML2 = YES ]; then
+ njs_module_name=qjs_xml_module
+ njs_module_incs=
+ njs_module_srcs=external/qjs_xml_module.c
+
+ . auto/qjs_module
+fi
+
if [ $NJS_ZLIB = YES -a $NJS_HAVE_ZLIB = YES ]; then
njs_module_name=qjs_zlib_module
njs_module_incs=
diff --git a/external/qjs_xml_module.c b/external/qjs_xml_module.c
new file mode 100644
index 00000000..087b4a7b
--- /dev/null
+++ b/external/qjs_xml_module.c
@@ -0,0 +1,2161 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) F5, Inc.
+ */
+
+#include <qjs.h>
+#include <njs_sprintf.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/c14n.h>
+#include <libxml/xpathInternals.h>
+
+
+typedef struct {
+ xmlDoc *doc;
+ xmlParserCtxt *ctx;
+ xmlNode *free;
+ int ref_count;
+} qjs_xml_doc_t;
+
+
+typedef struct {
+ xmlNode *node;
+ qjs_xml_doc_t *doc;
+} qjs_xml_node_t;
+
+
+typedef struct {
+ xmlAttr *attr;
+ qjs_xml_doc_t *doc;
+} qjs_xml_attr_t;
+
+
+typedef enum {
+ XML_NSET_TREE = 0,
+ XML_NSET_TREE_NO_COMMENTS,
+ XML_NSET_TREE_INVERT,
+} qjs_xml_nset_type_t;
+
+
+typedef struct qjs_xml_nset_s qjs_xml_nset_t;
+
+struct qjs_xml_nset_s {
+ xmlNodeSet *nodes;
+ xmlDoc *doc;
+ qjs_xml_nset_type_t type;
+ qjs_xml_nset_t *next;
+ qjs_xml_nset_t *prev;
+};
+
+
+static JSValue qjs_xml_parse(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv);
+static JSValue qjs_xml_canonicalization(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv, int magic);
+
+static int qjs_xml_doc_get_own_property(JSContext *cx,
+ JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int qjs_xml_doc_get_own_property_names(JSContext *cx,
+ JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+static void qjs_xml_doc_finalizer(JSRuntime *rt, JSValue val);
+static void qjs_xml_doc_free(JSRuntime *rt, qjs_xml_doc_t *current);
+
+static JSValue qjs_xml_node_make(JSContext *cx, qjs_xml_doc_t *doc,
+ xmlNode *node);
+static int qjs_xml_node_get_own_property(JSContext *cx,
+ JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int qjs_xml_node_get_own_property_names(JSContext *cx,
+ JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+static int qjs_xml_node_set_property(JSContext *cx, JSValueConst obj,
+ JSAtom atom, JSValueConst value, JSValueConst receiver, int flags);
+static int qjs_xml_node_delete_property(JSContext *cx, JSValueConst obj,
+ JSAtom prop);
+static int qjs_xml_node_define_own_property(JSContext *cx, JSValueConst obj,
+ JSAtom atom, JSValueConst value, JSValueConst getter, JSValueConst setter,
+ int flags);
+static JSValue qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_remove_children(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_set_attribute(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_remove_attribute(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_remove_all_attributes(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_set_text(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_remove_text(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv);
+static void qjs_xml_node_finalizer(JSRuntime *rt, JSValue val);
+static xmlNode *qjs_xml_node(JSContext *cx, JSValueConst val, xmlDoc **doc);
+
+static JSValue qjs_xml_attr_make(JSContext *cx, qjs_xml_doc_t *doc,
+ xmlAttr *attr);
+static int qjs_xml_attr_get_own_property(JSContext *cx,
+ JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int qjs_xml_attr_get_own_property_names(JSContext *cx,
+ JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+static void qjs_xml_attr_finalizer(JSRuntime *rt, JSValue val);
+
+static u_char **qjs_xml_parse_ns_list(JSContext *cx, u_char *src);
+static int qjs_xml_c14n_visibility_cb(void *user_data, xmlNode *node,
+ xmlNode *parent);
+static int qjs_xml_buf_write_cb(void *context, const char *buffer, int len);
+static qjs_xml_nset_t *qjs_xml_nset_create(JSContext *cx, xmlDoc *doc,
+ xmlNode *current, qjs_xml_nset_type_t type);
+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 int qjs_xml_encode_special_chars(JSContext *cx, njs_str_t *src,
+ njs_str_t *out);
+static void qjs_xml_replace_node(JSContext *cx, qjs_xml_node_t *node,
+ xmlNode *current);
+
+static void qjs_xml_error(JSContext *cx, qjs_xml_doc_t *current,
+ const char *fmt, ...);
+static JSModuleDef *qjs_xml_init(JSContext *ctx, const char *name);
+
+
+static const JSCFunctionListEntry qjs_xml_export[] = {
+ JS_CFUNC_DEF("parse", 2, qjs_xml_parse),
+ JS_CFUNC_MAGIC_DEF("c14n", 4, qjs_xml_canonicalization, 0),
+ JS_CFUNC_MAGIC_DEF("exclusiveC14n", 4, qjs_xml_canonicalization, 1),
+ JS_CFUNC_MAGIC_DEF("serialize", 4, qjs_xml_canonicalization, 0),
+ JS_CFUNC_MAGIC_DEF("serializeToString", 4, qjs_xml_canonicalization, 2),
+};
+
+
+static const JSCFunctionListEntry qjs_xml_doc_proto[] = {
+ JS_PROP_STRING_DEF("[Symbol.toStringTag]", "XMLDoc", JS_PROP_CONFIGURABLE),
+};
+
+
+static const JSCFunctionListEntry qjs_xml_node_proto[] = {
+ JS_PROP_STRING_DEF("[Symbol.toStringTag]", "XMLNode", JS_PROP_CONFIGURABLE),
+ JS_CFUNC_DEF("addChild", 1, qjs_xml_node_add_child),
+ JS_CFUNC_DEF("removeChildren", 1, qjs_xml_node_remove_children),
+ JS_CFUNC_DEF("setAttribute", 2, qjs_xml_node_set_attribute),
+ JS_CFUNC_DEF("removeAttribute", 1, qjs_xml_node_remove_attribute),
+ JS_CFUNC_DEF("removeAllAttributes", 0, qjs_xml_node_remove_all_attributes),
+ JS_CFUNC_DEF("setText", 1, qjs_xml_node_set_text),
+ JS_CFUNC_DEF("removeText", 0, qjs_xml_node_remove_text),
+};
+
+
+static const JSCFunctionListEntry qjs_xml_attr_proto[] = {
+ JS_PROP_STRING_DEF("[Symbol.toStringTag]", "XMLAttr", JS_PROP_CONFIGURABLE),
+};
+
+
+static JSClassDef qjs_xml_doc_class = {
+ "XMLDoc",
+ .finalizer = qjs_xml_doc_finalizer,
+ .exotic = & (JSClassExoticMethods) {
+ .get_own_property = qjs_xml_doc_get_own_property,
+ .get_own_property_names = qjs_xml_doc_get_own_property_names,
+ },
+};
+
+
+static JSClassDef qjs_xml_node_class = {
+ "XMLNode",
+ .finalizer = qjs_xml_node_finalizer,
+ .exotic = & (JSClassExoticMethods) {
+ .get_own_property = qjs_xml_node_get_own_property,
+ .get_own_property_names = qjs_xml_node_get_own_property_names,
+ .set_property = qjs_xml_node_set_property,
+ .delete_property = qjs_xml_node_delete_property,
+ .define_own_property = qjs_xml_node_define_own_property,
+ },
+};
+
+
+static JSClassDef qjs_xml_attr_class = {
+ "XMLAttr",
+ .finalizer = qjs_xml_attr_finalizer,
+ .exotic = & (JSClassExoticMethods) {
+ .get_own_property = qjs_xml_attr_get_own_property,
+ .get_own_property_names = qjs_xml_attr_get_own_property_names,
+ },
+};
+
+
+qjs_module_t qjs_xml_module = {
+ .name = "xml",
+ .init = qjs_xml_init,
+};
+
+
+static JSValue
+qjs_xml_parse(JSContext *cx, JSValueConst this_val, int argc,
+ JSValueConst *argv)
+{
+ JSValue ret;
+ qjs_bytes_t data;
+ qjs_xml_doc_t *tree;
+
+ if (qjs_to_bytes(cx, &data, argv[0]) < 0) {
+ return JS_EXCEPTION;
+ }
+
+ tree = js_mallocz(cx, sizeof(qjs_xml_doc_t));
+ if (tree == NULL) {
+ qjs_bytes_free(cx, &data);
+ JS_ThrowOutOfMemory(cx);
+ return JS_EXCEPTION;
+ }
+
+ tree->ref_count = 1;
+
+ tree->ctx = xmlNewParserCtxt();
+ if (tree->ctx == NULL) {
+ qjs_bytes_free(cx, &data);
+ JS_ThrowInternalError(cx, "xmlNewParserCtxt() failed");
+ qjs_xml_doc_free(JS_GetRuntime(cx), tree);
+ return JS_EXCEPTION;
+ }
+
+ tree->doc = xmlCtxtReadMemory(tree->ctx, (char *) data.start, data.length,
+ NULL, NULL, XML_PARSE_NOWARNING
+ | XML_PARSE_NOERROR);
+ qjs_bytes_free(cx, &data);
+ if (tree->doc == NULL) {
+ qjs_xml_error(cx, tree, "failed to parse XML");
+ qjs_xml_doc_free(JS_GetRuntime(cx), tree);
+ return JS_EXCEPTION;
+ }
+
+ ret = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_XML_DOC);
+ if (JS_IsException(ret)) {
+ qjs_xml_doc_free(JS_GetRuntime(cx), tree);
+ return ret;
+ }
+
+ JS_SetOpaque(ret, tree);
+
+ return ret;
+}
+
+
+static JSValue
+qjs_xml_canonicalization(JSContext *cx, JSValueConst this_val, int argc,
+ JSValueConst *argv, int magic)
+{
+ int comments;
+ u_char **prefix_list, *pref;
+ xmlDoc *doc;
+ xmlNode *node;
+ ssize_t size;
+ JSValue excluding, prefixes, ret;
+ njs_chb_t chain;
+ qjs_xml_node_t *nd;
+ qjs_xml_nset_t *nset, *children;
+ xmlOutputBuffer *buf;
+
+ node = qjs_xml_node(cx, argv[0], &doc);
+ if (node == NULL) {
+ return JS_EXCEPTION;
+ }
+
+ comments = JS_ToBool(cx, argv[2]);
+ if (comments < 0) {
+ return JS_EXCEPTION;
+ }
+
+ buf = NULL;
+ nset = NULL;
+ children = NULL;
+
+ excluding = argv[1];
+ if (!JS_IsNullOrUndefined(excluding)) {
+ nd = JS_GetOpaque(excluding, QJS_CORE_CLASS_ID_XML_NODE);
+ if (nd == NULL) {
+ JS_ThrowTypeError(cx, "\"excluding\" argument is not a XMLNode "
+ "object");
+ return JS_EXCEPTION;
+ }
+
+ nset = qjs_xml_nset_create(cx, doc, node, XML_NSET_TREE_NO_COMMENTS);
+ if (nset == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ children = qjs_xml_nset_create(cx, nd->doc->doc, nd->node,
+ XML_NSET_TREE_INVERT);
+ if (children == NULL) {
+ qjs_xml_nset_free(cx, nset);
+ return JS_ThrowOutOfMemory(cx);
+ }
+
+ nset = qjs_xml_nset_add(nset, children);
+
+ } else {
+ nset = qjs_xml_nset_create(cx, doc, node,
+ comments ? XML_NSET_TREE
+ : XML_NSET_TREE_NO_COMMENTS);
+ if (nset == NULL) {
+ return JS_ThrowOutOfMemory(cx);
+ }
+ }
+
+ prefix_list = NULL;
+ prefixes = argv[3];
+ if (!JS_IsNullOrUndefined(prefixes)) {
+ if (!JS_IsString(prefixes)) {
+ JS_ThrowTypeError(cx, "\"prefixes\" argument is not a string");
+ goto fail;
+ }
+
+ pref = (u_char *) JS_ToCString(cx, prefixes);
+ if (pref == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ goto fail;
+ }
+
+ prefix_list = qjs_xml_parse_ns_list(cx, pref);
+ if (prefix_list == NULL) {
+ goto fail;
+ }
+ }
+
+ NJS_CHB_CTX_INIT(&chain, cx);
+
+ buf = xmlOutputBufferCreateIO(qjs_xml_buf_write_cb, NULL,
+ &chain, NULL);
+ if (buf == NULL) {
+ JS_ThrowInternalError(cx, "xmlOutputBufferCreateIO() failed");
+ goto fail;
+ }
+
+ size = xmlC14NExecute(doc, qjs_xml_c14n_visibility_cb, nset,
+ magic & 0x1 ? XML_C14N_EXCLUSIVE_1_0 : XML_C14N_1_0,
+ prefix_list, comments, buf);
+
+ if (size < 0) {
+ njs_chb_destroy(&chain);
+ (void) xmlOutputBufferClose(buf);
+ JS_ThrowInternalError(cx, "xmlC14NExecute() failed");
+ goto fail;
+ }
+
+ if (magic & 0x2) {
+ ret = qjs_string_create_chb(cx, &chain);
+
+ } else {
+ ret = qjs_buffer_chb_alloc(cx, &chain);
+ njs_chb_destroy(&chain);
+ }
+
+ (void) xmlOutputBufferClose(buf);
+
+ qjs_xml_nset_free(cx, nset);
+ qjs_xml_nset_free(cx, children);
+
+ if (prefix_list != NULL) {
+ js_free(cx, prefix_list);
+ }
+
+ return ret;
+
+fail:
+
+ qjs_xml_nset_free(cx, nset);
+ qjs_xml_nset_free(cx, children);
+
+ if (prefix_list != NULL) {
+ js_free(cx, prefix_list);
+ }
+
+ return JS_EXCEPTION;
+}
+
+
+static int
+qjs_xml_doc_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+ JSValueConst obj, JSAtom prop)
+{
+ int any;
+ xmlNode *node;
+ njs_str_t name;
+ qjs_xml_doc_t *tree;
+
+ tree = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_DOC);
+ if (tree == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not an XMLDoc");
+ return -1;
+ }
+
+ name.start = (u_char *) JS_AtomToCString(cx, prop);
+ if (name.start == NULL) {
+ return -1;
+ }
+
+ name.length = njs_strlen(name.start);
+
+ any = (name.length == njs_strlen("$root")
+ && njs_strncmp(name.start, "$root", name.length) == 0);
+
+ for (node = xmlDocGetRootElement(tree->doc);
+ node != NULL;
+ node = node->next)
+ {
+ if (node->type != XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ if (!any) {
+ if (name.length != njs_strlen(node->name)
+ || njs_strncmp(name.start, node->name, name.length) != 0)
+ {
+ continue;
+ }
+ }
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = qjs_xml_node_make(cx, tree, node);
+ if (JS_IsException(pdesc->value)) {
+ return -1;
+ }
+ }
+
+ return 1;
+ }
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ return 0;
+}
+
+
+static int
+qjs_xml_push_string(JSContext *cx, JSValue obj, const char *start)
+{
+ JSAtom key;
+
+ key = JS_NewAtomLen(cx, start, njs_strlen(start));
+ if (key == JS_ATOM_NULL) {
+ return -1;
+ }
+
+ if (JS_DefinePropertyValue(cx, obj, key, JS_UNDEFINED,
+ JS_PROP_ENUMERABLE) < 0)
+ {
+ JS_FreeAtom(cx, key);
+ return -1;
+ }
+
+ JS_FreeAtom(cx, key);
+
+ return 0;
+}
+
+
+static int
+qjs_xml_doc_get_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+ uint32_t *plen, JSValueConst obj)
+{
+ int rc;
+ JSValue keys;
+ xmlNode *node;
+ qjs_xml_doc_t *tree;
+
+ tree = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_DOC);
+ if (tree == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not an XMLDoc");
+ return -1;
+ }
+
+ keys = JS_NewObject(cx);
+ if (JS_IsException(keys)) {
+ return -1;
+ }
+
+ for (node = xmlDocGetRootElement(tree->doc);
+ node != NULL;
+ node = node->next)
+ {
+ if (node->type != XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ if (qjs_xml_push_string(cx, keys, (char *) node->name) < 0) {
+ JS_FreeValue(cx, keys);
+ return -1;
+ }
+ }
+
+ rc = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK);
+
+ JS_FreeValue(cx, keys);
+
+ return rc;
+}
+
+
+static void
+qjs_xml_doc_free(JSRuntime *rt, qjs_xml_doc_t *current)
+{
+ xmlNode *node, *next;
+
+ if (--current->ref_count > 0) {
+ return;
+ }
+
+ node = current->free;
+
+ while (node != NULL) {
+ next = node->next;
+ xmlFreeNode(node);
+ node = next;
+ }
+
+ if (current->doc != NULL) {
+ xmlFreeDoc(current->doc);
+ }
+
+ if (current->ctx != NULL) {
+ xmlFreeParserCtxt(current->ctx);
+ }
+
+ js_free_rt(rt, current);
+}
+
+
+static void
+qjs_xml_doc_finalizer(JSRuntime *rt, JSValue val)
+{
+ qjs_xml_doc_t *current;
+
+ current = JS_GetOpaque(val, QJS_CORE_CLASS_ID_XML_DOC);
+
+ qjs_xml_doc_free(rt, current);
+}
+
+
+static JSValue
+qjs_xml_node_make(JSContext *cx, qjs_xml_doc_t *doc, xmlNode *node)
+{
+ JSValue ret;
+ qjs_xml_node_t *current;
+
+ current = js_malloc(cx, sizeof(qjs_xml_node_t));
+ if (current == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return JS_EXCEPTION;
+ }
+
+ current->node = node;
+ current->doc = doc;
+ doc->ref_count++;
+
+ ret = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_XML_NODE);
+ if (JS_IsException(ret)) {
+ js_free(cx, current);
+ return ret;
+ }
+
+ JS_SetOpaque(ret, current);
+
+ return ret;
+}
+
+
+static JSValue
+qjs_xml_node_tag_handler(JSContext *cx, qjs_xml_node_t *current,
+ njs_str_t *name)
+{
+ size_t size;
+ xmlNode *node;
+
+ node = current->node;
+
+ for (node = node->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;
+ }
+
+ return qjs_xml_node_make(cx, current->doc, node);
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_xml_node_tags_handler(JSContext *cx, qjs_xml_node_t *current,
+ njs_str_t *name)
+{
+ size_t size;
+ int32_t i;
+ xmlNode *node;
+ JSValue arr, ret;
+
+ arr = JS_NewArray(cx);
+ if (JS_IsException(arr)) {
+ return arr;
+ }
+
+ i = 0;
+ node = current->node;
+
+ for (node = node->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;
+ }
+
+ ret = qjs_xml_node_make(cx, current->doc, node);
+ if (JS_IsException(ret)) {
+ JS_FreeValue(cx, arr);
+ return JS_EXCEPTION;
+ }
+
+ if (JS_SetPropertyUint32(cx, arr, i++, ret) < 0) {
+ JS_FreeValue(cx, arr);
+ JS_FreeValue(cx, ret);
+ return JS_EXCEPTION;
+ }
+ }
+
+ return arr;
+}
+
+
+static JSValue
+qjs_xml_node_attr_handler(JSContext *cx, qjs_xml_node_t *current,
+ njs_str_t *name)
+{
+ size_t size;
+ xmlAttr *attr;
+ xmlNode *node;
+
+ node = current->node;
+
+ for (attr = node->properties; attr != NULL; attr = attr->next) {
+ if (attr->type != XML_ATTRIBUTE_NODE) {
+ continue;
+ }
+
+ size = njs_strlen(attr->name);
+
+ if (name->length > 0
+ && (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
+ && attr->children->content != NULL)
+ {
+ return JS_NewString(cx, (char *) attr->children->content);
+ }
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static int
+qjs_xml_node_attr_modify(JSContext *cx, JSValue current, const u_char *name,
+ JSValue setval)
+{
+ xmlAttr *attr;
+ const u_char *value;
+ qjs_xml_node_t *node;
+
+ node = JS_GetOpaque(current, QJS_CORE_CLASS_ID_XML_NODE);
+ if (node == NULL) {
+ return -1;
+ }
+
+ if (xmlValidateQName(name, 0) != 0) {
+ JS_ThrowTypeError(cx, "attribute name \"%s\" is not valid", name);
+ return -1;
+ }
+
+ if (JS_IsNullOrUndefined(setval)) {
+ attr = xmlHasProp(node->node, name);
+
+ if (attr != NULL) {
+ xmlRemoveProp(attr);
+ }
+
+ return 1;
+ }
+
+ value = (const u_char *) JS_ToCString(cx, setval);
+ if (value == NULL) {
+ return -1;
+ }
+
+ attr = xmlSetProp(node->node, name, value);
+ JS_FreeCString(cx, (char *) value);
+ if (attr == NULL) {
+ JS_ThrowInternalError(cx, "xmlSetProp() failed");
+ return -1;
+ }
+
+ return 1;
+}
+
+
+static int
+qjs_xml_node_tag_modify(JSContext *cx, JSValue obj, njs_str_t *name,
+ JSValue setval)
+{
+ size_t size;
+ xmlNode *node, *next, *copy;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ return -1;
+ }
+
+ if (!JS_IsNullOrUndefined(setval)) {
+ JS_ThrowInternalError(cx, "XMLNode.$tag$xxx is not assignable, "
+ "use addChild() or node.$tags = [node1, node2, ..] "
+ "syntax");
+ 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;
+ }
+
+ size = njs_strlen(node->name);
+
+ if (name->length > 0
+ && (name->length != size
+ || njs_strncmp(name->start, node->name, size) != 0))
+ {
+ continue;
+ }
+
+ xmlUnlinkNode(node);
+
+ node->next = current->doc->free;
+ current->doc->free = node;
+ }
+
+ qjs_xml_replace_node(cx, current, copy);
+
+ return 1;
+}
+
+
+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;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ return -1;
+ }
+
+ if (!JS_IsArray(cx, setval)) {
+ JS_ThrowTypeError(cx, "setval is not an array");
+ return -1;
+ }
+
+ length = JS_GetPropertyStr(cx, setval, "length");
+ if (JS_IsException(length)) {
+ return -1;
+ }
+
+ if (JS_ToInt32(cx, &len, length) < 0) {
+ return -1;
+ }
+
+ copy = xmlDocCopyNode(current->node, current->doc->doc,
+ 2 /* copy properties and namespaces */);
+ if (copy == NULL) {
+ JS_ThrowInternalError(cx, "xmlDocCopyNode() failed");
+ return -1;
+ }
+
+ for (i = 0; i < len; i++) {
+ v = JS_GetPropertyUint32(cx, setval, i);
+ if (JS_IsException(v)) {
+ goto error;
+ }
+
+ node = qjs_xml_node(cx, v, NULL);
+ JS_FreeValue(cx, v);
+ if (node == NULL) {
+ 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;
+ }
+ }
+
+ if (xmlReconciliateNs(current->doc->doc, copy) == -1) {
+ JS_ThrowInternalError(cx, "xmlReconciliateNs() failed");
+ goto error;
+ }
+
+ qjs_xml_replace_node(cx, current, copy);
+
+ return 1;
+
+error:
+
+ xmlFreeNode(copy);
+
+ return -1;
+}
+
+
+static int
+qjs_xml_node_text_handler(JSContext *cx, JSValue current, JSValue setval)
+{
+ xmlNode *copy;
+ njs_str_t content, enc;
+ qjs_xml_node_t *node;
+
+ enc.start = NULL;
+ enc.length = 0;
+
+ node = JS_GetOpaque(current, QJS_CORE_CLASS_ID_XML_NODE);
+ if (node == NULL) {
+ return -1;
+ }
+
+ if (!JS_IsNullOrUndefined(setval)) {
+ content.start = (u_char *) JS_ToCStringLen(cx, &content.length, setval);
+ if (content.start == NULL) {
+ return -1;
+ }
+
+ if (qjs_xml_encode_special_chars(cx, &content, &enc) < 0) {
+ JS_FreeCString(cx, (char *) content.start);
+ return -1;
+ }
+
+ JS_FreeCString(cx, (char *) content.start);
+ }
+
+ copy = xmlDocCopyNode(node->node, node->doc->doc, 1);
+ if (copy == NULL) {
+ if (enc.start != NULL) {
+ js_free(cx, enc.start);
+ }
+
+ JS_ThrowInternalError(cx, "xmlDocCopyNode() failed");
+ return -1;
+ }
+
+ xmlNodeSetContentLen(copy, enc.start, enc.length);
+
+ if (enc.start != NULL) {
+ js_free(cx, enc.start);
+ }
+
+ qjs_xml_replace_node(cx, node, copy);
+
+ return 1;
+}
+
+
+static int
+qjs_xml_node_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+ JSValueConst obj, JSAtom prop)
+{
+ u_char *text;
+ JSValue value;
+ xmlNode *node;
+ njs_str_t name, nm;
+ qjs_xml_node_t *current;
+
+ /*
+ * $tag$foo - the first tag child with the name "foo"
+ * $tags$foo - the all children with the name "foo" as an array
+ * $attr$foo - the attribute with the name "foo"
+ * foo - the same as $tag$foo
+ */
+
+ current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not an XMLNode");
+ return -1;
+ }
+
+ name.start = (u_char *) JS_AtomToCString(cx, prop);
+ if (name.start == NULL) {
+ return -1;
+ }
+
+ name.length = njs_strlen(name.start);
+ node = current->node;
+
+ if (name.length > 1 && name.start[0] == '$') {
+ if (name.length == njs_length("$attrs")
+ && njs_strncmp(&name.start[1], "attrs", njs_length("attrs")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ if (node->properties == NULL) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = qjs_xml_attr_make(cx, current->doc,
+ node->properties);
+ if (JS_IsException(pdesc->value)) {
+ return -1;
+ }
+ }
+
+ return 1;
+ }
+
+ if (name.length > njs_length("$attr$")
+ && njs_strncmp(&name.start[1], "attr$", njs_length("attr$")) == 0)
+ {
+ nm.length = name.length - njs_length("$attr$");
+ nm.start = name.start + njs_length("$attr$");
+
+ value = qjs_xml_node_attr_handler(cx, current, &nm);
+ JS_FreeCString(cx, (char *) name.start);
+ if (JS_IsException(value)) {
+ return -1;
+ }
+
+ if (JS_IsUndefined(value)) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = value;
+ }
+
+ return 1;
+ }
+
+ if (name.length == njs_length("$name")
+ && njs_strncmp(&name.start[1], "name", njs_length("name")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ if (node->type != XML_ELEMENT_NODE) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = JS_NewString(cx, (char *) node->name);
+ if (JS_IsException(pdesc->value)) {
+ return -1;
+ }
+ }
+
+ return 1;
+ }
+
+ if (name.length == njs_length("$ns")
+ && njs_strncmp(&name.start[1], "ns", njs_length("ns")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ if (node->ns == NULL || node->ns->href == NULL) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = JS_NewString(cx, (char *) node->ns->href);
+ if (JS_IsException(pdesc->value)) {
+ return -1;
+ }
+ }
+
+ return 1;
+ }
+
+ if (name.length == njs_length("$parent")
+ && njs_strncmp(&name.start[1], "parent", njs_length("parent")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ if (node->parent == NULL
+ || node->parent->type != XML_ELEMENT_NODE)
+ {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = qjs_xml_node_make(cx, current->doc,
+ node->parent);
+ if (JS_IsException(pdesc->value)) {
+ return -1;
+ }
+ }
+
+ return 1;
+ }
+
+ if (name.length == njs_length("$tags")
+ && njs_strncmp(&name.start[1], "tags", njs_length("tags")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ if (pdesc != NULL) {
+ nm.start = NULL;
+ nm.length = 0;
+
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = qjs_xml_node_tags_handler(cx, current, &nm);
+ if (JS_IsException(pdesc->value)) {
+ return -1;
+ }
+ }
+
+ return 1;
+ }
+
+ if (name.length > njs_length("$tags$")
+ && njs_strncmp(&name.start[1], "tags$", njs_length("tags$")) == 0)
+ {
+ nm.start = name.start + njs_length("$tags$");
+ nm.length = name.length - njs_length("$tags$");
+
+ value = qjs_xml_node_tags_handler(cx, current, &nm);
+ JS_FreeCString(cx, (char *) name.start);
+ if (JS_IsException(value)) {
+ return -1;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = value;
+ }
+
+ return 1;
+ }
+
+ if (name.length > njs_length("$tag$")
+ && njs_strncmp(&name.start[1], "tag$", njs_length("tag$")) == 0)
+ {
+ nm.length = name.length - njs_length("$tag$");
+ nm.start = name.start + njs_length("$tag$");
+ goto tag;
+ }
+
+ if (name.length == njs_length("$text")
+ && njs_strncmp(&name.start[1], "text", njs_length("text")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ text = xmlNodeGetContent(current->node);
+ if (text == NULL) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = JS_NewString(cx, (char *) text);
+ if (JS_IsException(pdesc->value)) {
+ xmlFree(text);
+ return -1;
+ }
+ }
+
+ xmlFree(text);
+
+ return 1;
+ }
+ }
+
+ nm = name;
+
+tag:
+
+ value = qjs_xml_node_tag_handler(cx, current, &nm);
+ JS_FreeCString(cx, (char *) name.start);
+ if (JS_IsException(value)) {
+ return -1;
+ }
+
+ if (JS_IsUndefined(value)) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = value;
+ }
+
+ return 1;
+}
+
+
+static int
+qjs_xml_node_get_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+ uint32_t *plen, JSValueConst obj)
+{
+ int rc;
+ JSValue keys;
+ xmlNode *node, *current;
+ qjs_xml_node_t *tree;
+
+ tree = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_NODE);
+ if (tree == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not an XMLNode");
+ return -1;
+ }
+
+ keys = JS_NewObject(cx);
+ if (JS_IsException(keys)) {
+ return -1;
+ }
+
+ current = tree->node;
+
+ if (current->name != NULL && current->type == XML_ELEMENT_NODE) {
+ if (qjs_xml_push_string(cx, keys, "$name") < 0) {
+ goto fail;
+ }
+ }
+
+ if (current->ns != NULL) {
+ if (qjs_xml_push_string(cx, keys, "$ns") < 0) {
+ goto fail;
+ }
+ }
+
+ if (current->properties != NULL) {
+ if (qjs_xml_push_string(cx, keys, "$attrs") < 0) {
+ goto fail;
+ }
+ }
+
+ if (current->children != NULL && current->children->content != NULL) {
+ if (qjs_xml_push_string(cx, keys, "$text") < 0) {
+ goto fail;
+ }
+ }
+
+ for (node = current->children; node != NULL; node = node->next) {
+ if (node->type != XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ if (qjs_xml_push_string(cx, keys, "$tags") < 0) {
+ goto fail;
+ }
+
+ break;
+ }
+
+ rc = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK);
+
+ JS_FreeValue(cx, keys);
+
+ return rc;
+
+fail:
+
+ JS_FreeValue(cx, keys);
+
+ return -1;
+}
+
+static int
+qjs_xml_node_set_property(JSContext *cx, JSValueConst obj, JSAtom atom,
+ JSValueConst value, JSValueConst receiver, int flags)
+{
+ return qjs_xml_node_define_own_property(cx, obj, atom, value,
+ JS_UNDEFINED, JS_UNDEFINED, flags);
+}
+
+
+static int
+qjs_xml_node_delete_property(JSContext *cx, JSValueConst obj, JSAtom prop)
+{
+ return qjs_xml_node_define_own_property(cx, obj, prop, JS_UNDEFINED,
+ JS_UNDEFINED, JS_UNDEFINED,
+ JS_PROP_THROW);
+}
+
+
+static int
+qjs_xml_node_define_own_property(JSContext *cx, JSValueConst obj, JSAtom atom,
+ JSValueConst value, JSValueConst getter, JSValueConst setter, int flags)
+{
+ int rc;
+ njs_str_t name, nm;
+
+ name.start = (u_char *) JS_AtomToCString(cx, atom);
+ if (name.start == NULL) {
+ return -1;
+ }
+
+ name.length = njs_strlen(name.start);
+
+ if (name.length > 1 && name.start[0] == '$') {
+ if (name.length > njs_length("$attr$")
+ && njs_strncmp(&name.start[1], "attr$", njs_length("attr$")) == 0)
+ {
+ nm.start = name.start + njs_length("$attr$");
+
+ rc = qjs_xml_node_attr_modify(cx, obj, nm.start, value);
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ return rc;
+ }
+
+ if (name.length > njs_length("$tag$")
+ && njs_strncmp(&name.start[1], "tag$", njs_length("tag$")) == 0)
+ {
+ nm.start = name.start + njs_length("$tag$");
+ nm.length = name.length - njs_length("$tag$");
+
+ rc = qjs_xml_node_tag_modify(cx, obj, &nm, value);
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ return rc;
+ }
+
+ if (name.length >= njs_length("$tags$")
+ && njs_strncmp(&name.start[1], "tags$", njs_length("tags$")) == 0)
+ {
+ nm.start = name.start + njs_length("$tags$");
+ nm.length = name.length - njs_length("$tags$");
+
+ rc = qjs_xml_node_tags_modify(cx, obj, &nm, value);
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ return rc;
+ }
+
+ if (name.length >= njs_length("$tags")
+ && njs_strncmp(&name.start[1], "tags", njs_length("tags")) == 0)
+ {
+ nm.start = name.start + njs_length("$tags");
+ nm.length = name.length - njs_length("$tags");
+
+ rc = qjs_xml_node_tags_modify(cx, obj, &nm, value);
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ return rc;
+ }
+
+ if (name.length == njs_length("$text")
+ && njs_strncmp(&name.start[1], "text", njs_length("text")) == 0)
+ {
+ JS_FreeCString(cx, (char *) name.start);
+
+ return qjs_xml_node_text_handler(cx, obj, value);
+ }
+ }
+
+ rc = qjs_xml_node_tag_modify(cx, obj, &name, value);
+ JS_FreeCString(cx, (char *) name.start);
+
+ return rc;
+}
+
+
+static JSValue
+qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ xmlNode *copy, *node, *rnode;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ JS_ThrowTypeError(cx, "\"this\" is not a XMLNode object");
+ 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");
+ 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;
+ }
+
+ qjs_xml_replace_node(cx, current, copy);
+
+ return JS_UNDEFINED;
+
+error:
+
+ xmlFreeNode(copy);
+
+ return JS_EXCEPTION;
+}
+
+
+static JSValue
+qjs_xml_node_remove_children(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ int rc;
+ njs_str_t name;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ JS_ThrowTypeError(cx, "\"this\" is not a XMLNode object");
+ return JS_EXCEPTION;
+ }
+
+ if (!JS_IsNullOrUndefined(argv[0])) {
+ if (!JS_IsString(argv[0])) {
+ JS_ThrowTypeError(cx, "selector is not a string");
+ return JS_EXCEPTION;
+ }
+
+ name.start = (u_char *) JS_ToCString(cx, argv[0]);
+ if (name.start == NULL) {
+ return JS_EXCEPTION;
+ }
+
+ name.length = njs_strlen(name.start);
+
+ } else {
+ name.start = NULL;
+ name.length = 0;
+ }
+
+ rc = qjs_xml_node_tag_modify(cx, this_val, &name, JS_UNDEFINED);
+
+ if (name.start != NULL) {
+ JS_FreeCString(cx, (char *) name.start);
+ }
+
+ if (rc < 0) {
+ return JS_EXCEPTION;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_xml_node_set_attribute(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ const u_char *name;
+
+ if (!JS_IsString(argv[0])) {
+ JS_ThrowTypeError(cx, "\"name\" argument is not a string");
+ return JS_EXCEPTION;
+ }
+
+ name = (const u_char *) JS_ToCString(cx, argv[0]);
+
+ if (qjs_xml_node_attr_modify(cx, this_val, name, argv[1]) < 0) {
+ JS_FreeCString(cx, (char *) name);
+ return JS_EXCEPTION;
+ }
+
+ JS_FreeCString(cx, (char *) name);
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_xml_node_remove_attribute(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ const u_char *name;
+
+ if (!JS_IsString(argv[0])) {
+ JS_ThrowTypeError(cx, "\"name\" argument is not a string");
+ return JS_EXCEPTION;
+ }
+
+ name = (const u_char *) JS_ToCString(cx, argv[0]);
+
+ if (qjs_xml_node_attr_modify(cx, this_val, name, JS_UNDEFINED) < 0) {
+ JS_FreeCString(cx, (char *) name);
+ return JS_EXCEPTION;
+ }
+
+ JS_FreeCString(cx, (char *) name);
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_xml_node_remove_all_attributes(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ xmlNode *node;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ JS_ThrowTypeError(cx, "\"this\" is not a XMLNode object");
+ return JS_EXCEPTION;
+ }
+
+ node = current->node;
+
+ if (node->properties != NULL) {
+ xmlFreePropList(node->properties);
+ node->properties = NULL;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_xml_node_set_text(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ if (qjs_xml_node_text_handler(cx, this_val, argv[0]) < 0) {
+ return JS_EXCEPTION;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+qjs_xml_node_remove_text(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ if (qjs_xml_node_text_handler(cx, this_val, JS_UNDEFINED) < 0) {
+ return JS_EXCEPTION;
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static void
+qjs_xml_node_finalizer(JSRuntime *rt, JSValue val)
+{
+ qjs_xml_node_t *node;
+
+ node = JS_GetOpaque(val, QJS_CORE_CLASS_ID_XML_NODE);
+
+ qjs_xml_doc_free(rt, node->doc);
+
+ js_free_rt(rt, node);
+}
+
+
+static xmlNode *
+qjs_xml_node(JSContext *cx, JSValueConst val, xmlDoc **doc)
+{
+ qjs_xml_doc_t *tree;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(val, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ tree = JS_GetOpaque(val, QJS_CORE_CLASS_ID_XML_DOC);
+ if (tree == NULL) {
+ JS_ThrowInternalError(cx, "'this' is not XMLNode or XMLDoc");
+ return NULL;
+ }
+
+ if (doc != NULL) {
+ *doc = tree->doc;
+ }
+
+ return xmlDocGetRootElement(tree->doc);
+ }
+
+ if (doc != NULL) {
+ *doc = current->doc->doc;
+ }
+
+ return current->node;
+}
+
+
+static JSValue
+qjs_xml_attr_make(JSContext *cx, qjs_xml_doc_t *doc, xmlAttr *attr)
+{
+ JSValue ret;
+ qjs_xml_attr_t *current;
+
+ current = js_malloc(cx, sizeof(qjs_xml_attr_t));
+ if (current == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return JS_EXCEPTION;
+ }
+
+ current->attr = attr;
+ current->doc = doc;
+ doc->ref_count++;
+
+ ret = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_XML_ATTR);
+ if (JS_IsException(ret)) {
+ js_free(cx, current);
+ return ret;
+ }
+
+ JS_SetOpaque(ret, current);
+
+ return ret;
+}
+
+
+static int
+qjs_xml_attr_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+ JSValueConst obj, JSAtom prop)
+{
+ size_t size;
+ u_char *text;
+ xmlAttr *attr;
+ njs_str_t name;
+ qjs_xml_attr_t *current;
+
+ current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_ATTR);
+ if (current == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not an XMLAttr");
+ return -1;
+ }
+
+ name.start = (u_char *) JS_AtomToCString(cx, prop);
+ if (name.start == NULL) {
+ return -1;
+ }
+
+ name.length = njs_strlen(name.start);
+
+ for (attr = current->attr; 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;
+ }
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ text = xmlNodeGetContent(attr->children);
+ if (text == NULL) {
+ return 0;
+ }
+
+ if (pdesc != NULL) {
+ pdesc->flags = JS_PROP_ENUMERABLE;
+ pdesc->getter = JS_UNDEFINED;
+ pdesc->setter = JS_UNDEFINED;
+ pdesc->value = JS_NewString(cx, (char *) text);
+ if (JS_IsException(pdesc->value)) {
+ xmlFree(text);
+ return -1;
+ }
+ }
+
+ xmlFree(text);
+
+ return 1;
+ }
+
+ JS_FreeCString(cx, (char *) name.start);
+
+ return 0;
+}
+
+
+static int
+qjs_xml_attr_get_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+ uint32_t *plen, JSValueConst obj)
+{
+ int rc;
+ JSValue keys;
+ xmlAttr *attr;
+ qjs_xml_attr_t *current;
+
+ current = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_XML_ATTR);
+ if (current == NULL) {
+ (void) JS_ThrowInternalError(cx, "\"this\" is not an XMLAttr");
+ return -1;
+ }
+
+ keys = JS_NewObject(cx);
+ if (JS_IsException(keys)) {
+ return -1;
+ }
+
+ for (attr = current->attr; attr != NULL; attr = attr->next) {
+ if (attr->type != XML_ATTRIBUTE_NODE) {
+ continue;
+ }
+
+ if (qjs_xml_push_string(cx, keys, (char *) attr->name) < 0) {
+ goto fail;
+ }
+ }
+
+ rc = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK);
+
+ JS_FreeValue(cx, keys);
+
+ return rc;
+
+fail:
+
+ JS_FreeValue(cx, keys);
+
+ return -1;
+}
+
+
+static void
+qjs_xml_attr_finalizer(JSRuntime *rt, JSValue val)
+{
+ qjs_xml_attr_t *attr;
+
+ attr = JS_GetOpaque(val, QJS_CORE_CLASS_ID_XML_ATTR);
+
+ qjs_xml_doc_free(rt, attr->doc);
+
+ js_free_rt(rt, attr);
+}
+
+
+static u_char **
+qjs_xml_parse_ns_list(JSContext *cx, u_char *src)
+{
+ u_char *p, **buf, **out;
+ size_t size, idx;
+
+ size = 8;
+ p = src;
+
+ buf = js_mallocz(cx, size * sizeof(char *));
+ if (buf == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return NULL;
+ }
+
+ out = buf;
+
+ while (*p != '\0') {
+ idx = out - buf;
+
+ if (idx >= size) {
+ size *= 2;
+
+ buf = js_realloc(cx, buf, size * sizeof(char *));
+ if (buf == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return NULL;
+ }
+
+ out = &buf[idx];
+ }
+
+ *out++ = p;
+
+ while (*p != ' ' && *p != '\0') {
+ p++;
+ }
+
+ if (*p == ' ') {
+ *p++ = '\0';
+ }
+ }
+
+ *out = NULL;
+
+ return buf;
+}
+
+
+static int
+qjs_xml_node_one_contains(qjs_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:
+ case XML_NSET_TREE_NO_COMMENTS:
+ if (in != 0) {
+ return 1;
+ }
+
+ if ((parent != NULL) && (parent->type == XML_ELEMENT_NODE)) {
+ return qjs_xml_node_one_contains(nset, parent, parent->parent);
+ }
+
+ return 0;
+
+ case XML_NSET_TREE_INVERT:
+ default:
+ if (in != 0) {
+ return 0;
+ }
+
+ if ((parent != NULL) && (parent->type == XML_ELEMENT_NODE)) {
+ return qjs_xml_node_one_contains(nset, parent, parent->parent);
+ }
+ }
+
+ return 1;
+}
+
+
+static int
+qjs_xml_c14n_visibility_cb(void *user_data, xmlNode *node, xmlNode *parent)
+{
+ int status;
+ qjs_xml_nset_t *n, *nset;
+
+ nset = user_data;
+
+ if (nset == NULL) {
+ return 1;
+ }
+
+ status = 1;
+
+ n = nset;
+
+ do {
+ if (status && !qjs_xml_node_one_contains(n, node, parent)) {
+ status = 0;
+ }
+
+ n = n->next;
+ } while (n != nset);
+
+ return status;
+}
+
+
+static int
+qjs_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 qjs_xml_nset_t *
+qjs_xml_nset_create(JSContext *cx, xmlDoc *doc, xmlNode *current,
+ qjs_xml_nset_type_t type)
+{
+ xmlNodeSet *nodes;
+ qjs_xml_nset_t *nset;
+
+ nset = js_mallocz(cx, sizeof(qjs_xml_nset_t));
+ if (nset == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return NULL;
+ }
+
+ nodes = xmlXPathNodeSetCreate(current);
+ if (nodes == NULL) {
+ js_free(cx, nset);
+ JS_ThrowOutOfMemory(cx);
+ return NULL;
+ }
+
+ nset->doc = doc;
+ nset->type = type;
+ nset->nodes = nodes;
+ nset->next = nset->prev = nset;
+
+ return nset;
+}
+
+
+static qjs_xml_nset_t *
+qjs_xml_nset_add(qjs_xml_nset_t *nset, qjs_xml_nset_t *add)
+{
+ if (nset == NULL) {
+ return add;
+ }
+
+ add->next = nset;
+ add->prev = nset->prev;
+ nset->prev->next = add;
+ nset->prev = add;
+
+ return nset;
+}
+
+
+static void
+qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset)
+{
+ if (nset == NULL) {
+ return;
+ }
+
+ if (nset->nodes != NULL) {
+ xmlXPathFreeNodeSet(nset->nodes);
+ }
+
+ js_free(cx, nset);
+}
+
+
+static int
+qjs_xml_encode_special_chars(JSContext *cx, njs_str_t *src, njs_str_t *out)
+{
+ size_t len;
+ u_char *p, *dst, *end;
+
+ len = 0;
+ end = src->start + src->length;
+
+ for (p = src->start; p < end; p++) {
+ if (*p == '<' || *p == '>') {
+ len += njs_length("<");
+ }
+
+ if (*p == '&' || *p == '\r') {
+ len += njs_length("&");
+ }
+
+ if (*p == '"') {
+ len += njs_length(""");
+ }
+
+ len += 1;
+ }
+
+ if (len == 0) {
+ out->start = NULL;
+ out->length = 0;
+
+ return 0;
+ }
+
+ out->start = js_malloc(cx, len);
+ if (out->start == NULL) {
+ JS_ThrowOutOfMemory(cx);
+ return -1;
+ }
+
+ 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 0;
+}
+
+
+static void
+qjs_xml_replace_node(JSContext *cx, qjs_xml_node_t *node, xmlNode *current)
+{
+ xmlNode *old;
+
+ old = node->node;
+
+ if (current != NULL) {
+ old = xmlReplaceNode(old, current);
+
+ } else {
+ xmlUnlinkNode(old);
+ }
+
+ old->next = node->doc->free;
+ node->doc->free = old;
+}
+
+
+static void
+qjs_xml_error(JSContext *cx, qjs_xml_doc_t *current, const char *fmt, ...)
+{
+ u_char *p, *last;
+ va_list args;
+ const xmlError *err;
+ u_char errstr[NJS_MAX_ERROR_STR];
+
+ last = &errstr[NJS_MAX_ERROR_STR];
+
+ va_start(args, fmt);
+ p = njs_vsprintf(errstr, last - 1, fmt, args);
+ va_end(args);
+
+ err = xmlCtxtGetLastError(current->ctx);
+
+ if (err != NULL) {
+ p = njs_sprintf(p, last - 1, " (libxml2: \"%*s\" at %d:%d)",
+ njs_strlen(err->message) - 1, err->message, err->line,
+ err->int2);
+ }
+
+ JS_ThrowSyntaxError(cx, "%.*s", (int) (p - errstr), errstr);
+}
+
+
+static int
+qjs_xml_module_init(JSContext *cx, JSModuleDef *m)
+{
+ JSValue proto;
+
+ proto = JS_NewObject(cx);
+ if (JS_IsException(proto)) {
+ return -1;
+ }
+
+ JS_SetPropertyFunctionList(cx, proto, qjs_xml_export,
+ njs_nitems(qjs_xml_export));
+
+ if (JS_SetModuleExport(cx, m, "default", proto) != 0) {
+ return -1;
+ }
+
+ return JS_SetModuleExportList(cx, m, qjs_xml_export,
+ njs_nitems(qjs_xml_export));
+}
+
+
+static JSModuleDef *
+qjs_xml_init(JSContext *cx, const char *name)
+{
+ int rc;
+ JSValue proto;
+ JSModuleDef *m;
+
+ if (!JS_IsRegisteredClass(JS_GetRuntime(cx),
+ QJS_CORE_CLASS_ID_XML_DOC))
+ {
+ if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_XML_DOC,
+ &qjs_xml_doc_class) < 0)
+ {
+ return NULL;
+ }
+
+ proto = JS_NewObject(cx);
+ if (JS_IsException(proto)) {
+ return NULL;
+ }
+
+ JS_SetPropertyFunctionList(cx, proto, qjs_xml_doc_proto,
+ njs_nitems(qjs_xml_doc_proto));
+
+ JS_SetClassProto(cx, QJS_CORE_CLASS_ID_XML_DOC, proto);
+
+ if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_XML_NODE,
+ &qjs_xml_node_class) < 0)
+ {
+ return NULL;
+ }
+
+ proto = JS_NewObject(cx);
+ if (JS_IsException(proto)) {
+ return NULL;
+ }
+
+ JS_SetPropertyFunctionList(cx, proto, qjs_xml_node_proto,
+ njs_nitems(qjs_xml_node_proto));
+
+ JS_SetClassProto(cx, QJS_CORE_CLASS_ID_XML_NODE, proto);
+
+ if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_XML_ATTR,
+ &qjs_xml_attr_class) < 0)
+ {
+ return NULL;
+ }
+
+ proto = JS_NewObject(cx);
+ if (JS_IsException(proto)) {
+ return NULL;
+ }
+
+ JS_SetPropertyFunctionList(cx, proto, qjs_xml_attr_proto,
+ njs_nitems(qjs_xml_attr_proto));
+
+ JS_SetClassProto(cx, QJS_CORE_CLASS_ID_XML_ATTR, proto);
+ }
+
+ m = JS_NewCModule(cx, name, qjs_xml_module_init);
+ if (m == NULL) {
+ return NULL;
+ }
+
+ JS_AddModuleExport(cx, m, "default");
+ rc = JS_AddModuleExportList(cx, m, qjs_xml_export,
+ njs_nitems(qjs_xml_export));
+ if (rc != 0) {
+ return NULL;
+ }
+
+ return m;
+}
diff --git a/src/qjs.h b/src/qjs.h
index c7ef4de0..25d6cba3 100644
--- a/src/qjs.h
+++ b/src/qjs.h
@@ -44,7 +44,10 @@
#define QJS_CORE_CLASS_ID_WEBCRYPTO_KEY (QJS_CORE_CLASS_ID_OFFSET + 7)
#define QJS_CORE_CLASS_CRYPTO_HASH (QJS_CORE_CLASS_ID_OFFSET + 8)
#define QJS_CORE_CLASS_CRYPTO_HMAC (QJS_CORE_CLASS_ID_OFFSET + 9)
-#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 10)
+#define QJS_CORE_CLASS_ID_XML_DOC (QJS_CORE_CLASS_ID_OFFSET + 10)
+#define QJS_CORE_CLASS_ID_XML_NODE (QJS_CORE_CLASS_ID_OFFSET + 11)
+#define QJS_CORE_CLASS_ID_XML_ATTR (QJS_CORE_CLASS_ID_OFFSET + 12)
+#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 13)
typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name);
diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c
index f39660f5..980fd7fa 100644
--- a/src/test/njs_unit_test.c
+++ b/src/test/njs_unit_test.c
@@ -20239,279 +20239,6 @@ static njs_unit_test_t njs_fs_module_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(NJS_XML_DOC
- "[doc.note.$name,"
- " doc.note.to.$text,"
- " doc.note.$parent,"
- " doc.note.to.$parent.$name,"
- " doc.note.$tag$to.$text,"
- " doc.note.to.$attr$b,"
- " doc.note.$tags[1].$text,"
- " doc.note.$tags$from[0].$text]"),
- njs_str("note,Tove,,note,Tove,bar,Jani,Jani") },
-
- { njs_str("const xml = require('xml');"
- "let doc = xml.parse(`<root><foo>FOO</foo><foo>BAR</foo></root>`);"
- "[doc.root.$tags$foo[0].$text,"
- " doc.root.$tags$foo[1].$text,"
- " doc.root.$tags$bar.length,"
- " doc.root.$tags$.length]"),
- njs_str("FOO,BAR,0,2") },
-
- { njs_str("const xml = require('xml');"
- "let doc = xml.parse(`GARBAGE`)"),
- njs_str("Error: failed to parse XML (libxml2: \"Start tag expected, '<' not found\" at 1:1)") },
-
- { njs_str("const xml = require('xml');"
- "let doc = xml.parse(`<r><a></a>TEXT</r>`);"
- "doc.r.$text"),
- njs_str("TEXT") },
-
- { njs_str("const xml = require('xml');"
- "let doc = xml.parse(`<r>俄语<a></a>данные</r>`);"
- "doc.r.$text[2]"),
- njs_str("д") },
-
- { njs_str("const xml = require('xml');"
- "let doc = xml.parse(`<俄语 լեզու=\"ռուսերեն\">данные</俄语>`);"
- "[doc['俄语'].$name[1],"
- " doc['俄语']['$attr$լեզու'][7],"
- " doc['俄语'].$text[5]]"),
- njs_str("语,ն,е") },
-
- { njs_str("const xml = require('xml');"
- "var doc = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"><n1:elem1 xmlns:n1=\"http://b\">"
- "<!-- comment -->foo</n1:elem1></n0:pdu>`);"
- "[xml.c14n(doc.pdu.elem1),"
- " xml.exclusiveC14n(doc.pdu.elem1),"
- " xml.exclusiveC14n(doc.pdu.elem1, null, 1),"
- " xml.exclusiveC14n(doc.pdu.elem1, null, false, 'n0 n1')]"
- ".map(v => (new TextDecoder().decode(v)))"),
- njs_str("<n1:elem1 xmlns:n0=\"http://a\" xmlns:n1=\"http://b\">foo</n1:elem1>,"
- "<n1:elem1 xmlns:n1=\"http://b\">foo</n1:elem1>,"
- "<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(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(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(NJS_XML_DOC
- "njs.dump(doc)"),
- njs_str("XMLDoc {note:XMLNode {$name:'note',"
- "$tags:[XMLNode {$name:'to',"
- "$attrs:XMLAttr {b:'bar',a:'foo'},"
- "$text:'Tove'},"
- "XMLNode {$name:'from',$text:'Jani'}]}}") },
-
- { njs_str(NJS_XML_DOC
- "JSON.stringify(doc)"),
- njs_str("{\"note\":{\"$name\":\"note\",\"$tags\":"
- "[{\"$name\":\"to\",\"$attrs\":{\"b\":\"bar\",\"a\":\"foo\"},"
- "\"$text\":\"Tove\"},{\"$name\":\"from\",\"$text\":\"Jani\"}]}}") },
-
- { njs_str("var xml = require('xml');"
- "var doc = xml.parse(`<r></r>`); xml.exclusiveC14n(doc, 1)"),
- njs_str("TypeError: \"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><WA&KA></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><WA&KA></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=\"<bar"\">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("TypeError: attribute name \"<\" is not valid") },
-
- { njs_str(NJS_XML_DOC
- "doc.$root.$text = 'WAKA';"
- "doc.$root['$attr$' + 'x'.repeat(1024)] = 1;"),
- njs_str("InternalError: 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>") },
-};
-
-
static njs_unit_test_t njs_module_test[] =
{
{ njs_str("function f(){return 2}; var f; f()"),
@@ -23288,17 +23015,6 @@ static njs_test_suite_t njs_suites[] =
njs_nitems(njs_disabled_denormals_test),
njs_disabled_denormals_tests },
- {
-#if (NJS_HAVE_LIBXML2 && !NJS_HAVE_MEMORY_SANITIZER)
- njs_str("xml"),
-#else
- njs_str(""),
-#endif
- { .externals = 1, .repeat = 1, .unsafe = 1 },
- njs_xml_test,
- njs_nitems(njs_xml_test),
- njs_unit_test },
-
{ njs_str("module"),
{ .repeat = 1, .module = 1, .unsafe = 1 },
njs_module_test,
diff --git a/test/xml/external_entity_ignored.t.js b/test/xml/external_entity_ignored.t.js
index 57c91e7f..88b192f2 100644
--- a/test/xml/external_entity_ignored.t.js
+++ b/test/xml/external_entity_ignored.t.js
@@ -1,9 +1,11 @@
/*---
-includes: [compatXml.js, compatNjs.js]
+includes: [compatNjs.js]
flags: []
paths: []
---*/
+import xml from 'xml';
+
let data = `<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY c PUBLIC "bar" "extern_entity.txt">
@@ -11,7 +13,7 @@ let data = `<?xml version="1.0"?>
<root>&c;</root>
`;
-if (has_njs() && has_xml()) {
+if (has_njs()) {
let doc = xml.parse(data);
assert.sameValue(doc.$root.$text, "");
}
diff --git a/test/xml/saml_verify.t.mjs b/test/xml/saml_verify.t.mjs
index d6f06a14..65d4e06b 100644
--- a/test/xml/saml_verify.t.mjs
+++ b/test/xml/saml_verify.t.mjs
@@ -1,8 +1,10 @@
/*---
-includes: [compatFs.js, compatXml.js, compatWebcrypto.js, compatNjs.js, runTsuite.js]
+includes: [compatFs.js, compatWebcrypto.js, compatNjs.js, runTsuite.js]
flags: [async]
---*/
+import xml from 'xml';
+
async function verify(params) {
let file_data = fs.readFileSync(`test/xml/${params.saml}`);
let key_data = fs.readFileSync(`test/webcrypto/${params.key.file}`);
@@ -11,14 +13,13 @@ async function verify(params) {
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)
.catch (e => {
- if (e.toString().startsWith("Error: EVP_PKEY_CTX_set_signature_md() failed")) {
+ if (e.message.startsWith("EVP_PKEY_CTX_set_signature_md() failed")) {
/* Red Hat Enterprise Linux: SHA-1 is disabled */
return "SKIPPED";
}
@@ -273,7 +274,7 @@ async function signatureSAML(signature, key_data, produce) {
let saml_verify_tsuite = {
name: "SAML verify",
- skip: () => (!has_njs() || !has_webcrypto() || !has_xml()),
+ skip: () => (!has_njs() || !has_webcrypto()),
T: verify,
opts: {
key: { fmt: "spki", file: "rsa.spki" },
diff --git a/test/xml/xml.t.mjs b/test/xml/xml.t.mjs
new file mode 100644
index 00000000..29e2fb41
--- /dev/null
+++ b/test/xml/xml.t.mjs
@@ -0,0 +1,385 @@
+/*---
+includes: [compatFs.js, compatNjs.js, runTsuite.js]
+flags: [async]
+---*/
+
+import xml from 'xml';
+
+let parse_tsuite = {
+ name: "parse()",
+ skip: () => (!has_njs()),
+ T: async (params) => {
+ let doc = xml.parse(params.doc);
+ let r = params.get(doc);
+
+ if (r !== params.expected) {
+ throw Error(`unexpected output "${r}" != "${params.expected}"`);
+ }
+
+ return 'SUCCESS';
+ },
+
+ opts: {
+ doc: `<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`,
+ },
+
+ tests: [
+ { get: (doc) => doc.nonexist, expected: undefined },
+ { get: (doc) => doc.note.$name, expected: 'note' },
+ { get: (doc) => doc.note.$text, expected: 'ToveJani' },
+ { get: (doc) => doc.note.to.$text, expected: 'Tove' },
+ { get: (doc) => doc.note.$tag$to.$text, expected: 'Tove' },
+ { get: (doc) => doc.note.$attrs, expected: undefined },
+ { get: (doc) => doc.note.to.$attrs.a, expected: 'foo' },
+ { get: (doc) => doc.note.to.$attrs.b, expected: 'bar' },
+ { get: (doc) => doc.note.to.$attr$b, expected: 'bar' },
+ { get: (doc) => doc.note.$attr$a, expected: undefined },
+ { get: (doc) => Array.isArray(doc.note.$tags), expected: true },
+ { get: (doc) => doc.note.$tags[0].$text, expected: 'Tove' },
+ { get: (doc) => doc.note.$tags[1].$text, expected: 'Jani' },
+ { get: (doc) => doc.note.$tags$from[0].$text, expected: 'Jani' },
+ { get: (doc) => doc.note.$parent, expected: undefined },
+ { get: (doc) => doc.note.to.$parent.$name, expected: 'note' },
+ { doc: `<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`,
+ get: (doc) => doc.pdu.$ns,
+ expected: 'http://a' },
+ { doc: `<root><foo>FOO</foo><foo>BAR</foo></root>`,
+ get: (doc) => doc.root.$tags$foo[0].$text,
+ expected: 'FOO' },
+ { doc: `<root><foo>FOO</foo><foo>BAR</foo></root>`,
+ get: (doc) => doc.root.$tags$foo[1].$text,
+ expected: 'BAR' },
+ { doc: `<root><foo>FOO</foo><foo>BAR</foo></root>`,
+ get: (doc) => doc.root.$tags$bar.length,
+ expected: 0 },
+ { doc: `<root><foo>FOO</foo><foo>BAR</foo></root>`,
+ get: (doc) => doc.root.$tags.length,
+ expected: 2 },
+ { doc: `<r><a></a>TEXT</r>`,
+ get: (doc) => doc.r.$text,
+ expected: 'TEXT' },
+ { doc: `<r><a></a>TEXT</r>`,
+ get: (doc) => doc.$root.$text,
+ expected: 'TEXT' },
+ { doc: `<r>俄语<a></a>данные</r>`,
+ get: (doc) => doc.r.$text[2],
+ expected: 'д' },
+ { doc: `<俄语 լեզու=\"ռուսերեն\">данные</俄语>`,
+ get: (doc) => JSON.stringify([doc['俄语'].$name[1],doc['俄语']['$attr$լեզու'][7],doc['俄语'].$text[5]]),
+ expected: '["语","ն","е"]' },
+
+ { get: (doc) => JSON.stringify(Object.getOwnPropertyNames(doc)),
+ expected: '["note"]' },
+ { get: (doc) => JSON.stringify(Object.getOwnPropertyNames(doc.note)),
+ expected: '["$name","$tags"]' },
+ { get: (doc) => JSON.stringify(Object.getOwnPropertyNames(doc.note.to)),
+ expected: '["$name","$attrs","$text"]' },
+
+ { get: (doc) => JSON.stringify(doc.note.to.$attrs),
+ expected: '{"b":"bar","a":"foo"}' },
+ { get: (doc) => JSON.stringify(doc.note.$tags),
+ expected: '[{"$name":"to","$attrs":{"b":"bar","a":"foo"},"$text":"Tove"},{"$name":"from","$text":"Jani"}]' },
+ { get: (doc) => JSON.stringify(doc),
+ expected: '{"note":{"$name":"note","$tags":[{"$name":"to","$attrs":{"b":"bar","a":"foo"},"$text":"Tove"},{"$name":"from","$text":"Jani"}]}}' },
+ { get: (doc) => JSON.stringify(doc.note),
+ expected: '{"$name":"note","$tags":[{"$name":"to","$attrs":{"b":"bar","a":"foo"},"$text":"Tove"},{"$name":"from","$text":"Jani"}]}' },
+
+ { doc: `GARBAGE`,
+ exception: 'Error: failed to parse XML (libxml2: "Start tag expected, \'<\' not found" at 1:1)' },
+]};
+
+let c14n_tsuite = {
+ name: "c14n()",
+ skip: () => (!has_njs()),
+ T: async (params) => {
+ let doc = xml.parse(params.doc);
+ let r = params.call(doc);
+
+ if (params.buffer) {
+ r = new TextDecoder().decode(r);
+ }
+
+ if (r !== params.expected) {
+ throw Error(`unexpected output "${r}" != "${params.expected}"`);
+ }
+
+ return 'SUCCESS';
+ },
+
+ opts: {
+ buffer: true,
+ doc: `<n0:pdu xmlns:n0=\"http://a\"><n1:elem1 xmlns:n1=\"http://b\"><!-- comment -->foo</n1:elem1></n0:pdu>`,
+ },
+
+ tests: [
+ { call: (doc) => xml.c14n(doc.pdu.elem1),
+ expected: `<n1:elem1 xmlns:n0="http://a" xmlns:n1="http://b">foo</n1:elem1>` },
+ { call: (doc) => xml.serialize(doc.pdu.elem1),
+ expected: `<n1:elem1 xmlns:n0="http://a" xmlns:n1="http://b">foo</n1:elem1>` },
+ { call: (doc) => xml.serializeToString(doc.pdu.elem1),
+ buffer: false,
+ expected: `<n1:elem1 xmlns:n0="http://a" xmlns:n1="http://b">foo</n1:elem1>` },
+ { call: (doc) => xml.exclusiveC14n(doc.pdu.elem1),
+ expected: `<n1:elem1 xmlns:n1="http://b">foo</n1:elem1>` },
+ { call: (doc) => xml.exclusiveC14n(doc.pdu.elem1, null, true),
+ expected: `<n1:elem1 xmlns:n1="http://b"><!-- comment -->foo</n1:elem1>` },
+ { call: (doc) => xml.exclusiveC14n(doc.pdu.elem1, null, false, 'n1'),
+ expected: `<n1:elem1 xmlns:n1="http://b">foo</n1:elem1>` },
+ { call: (doc) => xml.exclusiveC14n(doc.pdu.elem1, null, false, 'a b c d e f g h i j'),
+ expected: `<n1:elem1 xmlns:n1="http://b">foo</n1:elem1>` },
+ { doc: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>`,
+ call: (doc) => xml.c14n(doc.note),
+ expected: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>` },
+ { doc: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>`,
+ call: (doc) => xml.exclusiveC14n(doc.note),
+ expected: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>` },
+ { 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></r>`,
+ call: (doc) => xml.exclusiveC14n(doc, 1),
+ exception: 'TypeError: "excluding" argument is not a XMLNode object' },
+]};
+
+let modify_tsuite = {
+ name: "modifying XML",
+ skip: () => (!has_njs()),
+ T: async (params) => {
+ let doc = xml.parse(params.doc);
+ let r = params.get(doc);
+
+ if (r !== params.expected) {
+ throw Error(`unexpected output "${r}" != "${params.expected}"`);
+ }
+
+ return 'SUCCESS';
+ },
+
+ opts: {
+ doc: `<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`,
+ },
+
+ tests: [
+ { get: (doc) => {
+ doc.note.setText('WAKA');
+ return doc.note.$text;
+ },
+ expected: 'WAKA' },
+ { get: (doc) => {
+ doc.note.setText('WAKA');
+ return xml.serializeToString(doc);
+ },
+ expected: `<note>WAKA</note>` },
+ { get: (doc) => {
+ doc.note.$text = '<WA&KA>';
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><WA&KA></note>` },
+ { get: (doc) => {
+ doc.note.$text = '';
+ return xml.serializeToString(doc);
+ },
+ expected: `<note></note>` },
+ { get: (doc) => {
+ doc.note.setText('<WA&KA>');
+ return doc.note.$text;
+ },
+ expected: '<WA&KA>' },
+ { get: (doc) => {
+ doc.note.setText('<WA&KA>');
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><WA&KA></note>` },
+ { get: (doc) => {
+ doc.note.setText('"WAKA"');
+ return xml.serializeToString(doc);
+ },
+ expected: `<note>"WAKA"</note>` },
+ { get: (doc) => {
+ doc.note.setText('');
+ return doc.note.$text;
+ },
+ expected: '' },
+ { get: (doc) => {
+ doc.note.setText('');
+ return xml.serializeToString(doc);
+ },
+ expected: `<note></note>` },
+ { get: (doc) => {
+ doc.note.setText(null);
+ return doc.note.$text;
+ },
+ expected: '' },
+ { get: (doc) => {
+ doc.note.setText(undefined);
+ return doc.note.$text;
+ },
+ expected: '' },
+ { get: (doc) => {
+ doc.note.removeText();
+ return doc.note.$text;
+ },
+ expected: '' },
+ { get: (doc) => {
+ delete doc.note.$text;
+ return xml.serializeToString(doc);
+ },
+ expected: `<note></note>` },
+ { get: (doc) => {
+ doc.note.removeText();
+ return xml.serializeToString(doc);
+ },
+ expected: `<note></note>` },
+ { 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();
+ },
+ expected: 'to,Tove,bar,note' },
+ { get: (doc) => {
+ doc.note.to.setAttribute('aaa', 'foo');
+ doc.note.to.setAttribute('bbb', '<bar\"');
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to a="foo" aaa="foo" b="bar" bbb="<bar"">Tove</to>` },
+ { get: (doc) => {
+ doc.note.to.$attr$aaa = 'foo';
+ doc.note.to.$attr$bbb = '<bar\"';
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to a="foo" aaa="foo" b="bar" bbb="<bar"">Tove</to>` },
+ { get: (doc) => {
+ doc.note.to.$attr$aaa = 'foo';
+ return doc.note.to.$attr$aaa;
+ },
+ expected: `foo` },
+ { get: (doc) => {
+ doc.note.to.setAttribute('aaa', 'foo');
+ doc.note.to.setAttribute('aaa', 'foo2');
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to a="foo" aaa="foo2" b="bar">Tove</to>` },
+ { get: (doc) => {
+ doc.note.to.removeAttribute('a');
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to b="bar">Tove</to>` },
+ { get: (doc) => {
+ doc.note.to.removeAllAttributes();
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to>Tove</to>` },
+ { get: (doc) => {
+ delete doc.note.to.$attr$a;
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to b="bar">Tove</to>` },
+ { get: (doc) => {
+ doc.note.to.setAttribute('a', null);
+ return xml.serializeToString(doc.note.to);
+ },
+ expected: `<to b="bar">Tove</to>` },
+ { get: (doc) => {
+ doc.note.to.setAttribute('<', 'foo');
+ return xml.serializeToString(doc.note.to);
+ },
+ exception: 'TypeError: attribute name "<" is not valid' },
+ { get: (doc) => {
+ let doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);
+ 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>` },
+ { get: (doc) => {
+ let doc2 = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`);
+ 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>` },
+ { get: (doc) => {
+ doc.note.removeChildren('to');
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><from>Jani</from></note>` },
+ { get: (doc) => {
+ delete doc.note.$tag$to;
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><from>Jani</from></note>` },
+ { get: (doc) => {
+ delete doc.note.to;
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><from>Jani</from></note>` },
+ { get: (doc) => {
+ doc.note.removeChildren('xxx');
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>` },
+ { get: (doc) => {
+ delete doc.note.$tag$xxx;
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>` },
+ { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
+ get: (doc) => {
+ doc.$root.removeChildren('a');
+ return xml.serializeToString(doc);
+ },
+ expected: `<root><b>B</b></root>` },
+ { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
+ get: (doc) => {
+ doc.$root.removeChildren();
+ return xml.serializeToString(doc);
+ },
+ expected: `<root></root>` },
+ { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
+ get: (doc) => {
+ doc.$root.$tags = [];
+ return xml.serializeToString(doc);
+ },
+ expected: `<root></root>` },
+ { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
+ get: (doc) => {
+ doc.$root.$tags$ = [];
+ return xml.serializeToString(doc);
+ },
+ expected: `<root></root>` },
+ { doc: `<root><a>A</a><b>B</b><a>C</a></root>`,
+ get: (doc) => {
+ delete doc.$root.$tag$a;
+ return xml.serializeToString(doc);
+ },
+ expected: `<root><b>B</b></root>` },
+ { get: (doc) => {
+ doc.note.$tags = [doc.note.to];
+ return xml.serializeToString(doc);
+ },
+ expected: `<note><to a="foo" b="bar">Tove</to></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>` },
+ { 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>` },
+]};
+
+run([
+ parse_tsuite,
+ c14n_tsuite,
+ modify_tsuite,
+])
+.then($DONE, $DONE);
More information about the nginx-devel
mailing list