details: https://github.com/nginx/njs/commit/cec9a1650a5a46bbede4a0b7abd750d25df19f28 branches: master commit: cec9a1650a5a46bbede4a0b7abd750d25df19f28 user: Dmitry Volyntsev <xei...@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); _______________________________________________ nginx-devel mailing list nginx-devel@nginx.org https://mailman.nginx.org/mailman/listinfo/nginx-devel